diff --git a/.cargo/config.toml b/.cargo/config.toml index d4c169c4..bf53f2be 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,2 @@ [patch.crates-io] -rapier3d = { path = "impulse-rapier/src/main/rust/target/impulse-patched/rapier3d-0.32.0-impulse" } +rapier3d = { path = "impulse-backends/rapier/src/main/rust/target/impulse-patched/rapier3d-0.32.0-impulse" } diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 67ef3d7c..29d238d4 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -34,11 +34,11 @@ body: label: Validation already run placeholder: "./gradlew headlessTest" - type: textarea - id: crucible-test + id: runtime-reproduction attributes: - label: Crucible reproduction - description: If this is a runtime/server bug, include the failing Crucible test or describe the test case that should be added. - placeholder: "Example: add a focused Crucible smoke that unloads/reloads the affected plugin after spawning the failing entity state." + label: Runtime reproduction + description: If this is a runtime/server bug, include the focused regression test or describe the manual runAllMods scenario. + placeholder: "Example: runAllMods, create space --backend=impulse:rapier, spawn the failing entity state, then unload/reload the affected plugin." - type: textarea id: logs attributes: diff --git a/.github/workflows/backend-artifacts.yml b/.github/workflows/backend-artifacts.yml index 075d897e..104cce75 100644 --- a/.github/workflows/backend-artifacts.yml +++ b/.github/workflows/backend-artifacts.yml @@ -7,6 +7,66 @@ permissions: contents: read jobs: + jolt-native: + name: Jolt native ${{ matrix.platform }} + runs-on: ${{ matrix.runner }} + + strategy: + fail-fast: false + matrix: + include: + - platform: linux-x86_64 + runner: ubuntu-22.04 + resource-path: linux/x86_64 + library: libimpulse_jolt.so + gradle-command: ./gradlew :impulse-backends:jolt:stageJoltNativeResource + - platform: linux-arm64 + runner: ubuntu-24.04-arm + resource-path: linux/arm64 + library: libimpulse_jolt.so + gradle-command: ./gradlew :impulse-backends:jolt:stageJoltNativeResource + - platform: osx-arm64 + runner: macos-15 + resource-path: osx/arm64 + library: libimpulse_jolt.dylib + gradle-command: ./gradlew :impulse-backends:jolt:stageJoltNativeResource + - platform: windows-x86_64 + runner: windows-2025 + resource-path: windows/x86_64 + library: impulse_jolt.dll + gradle-command: ./gradlew.bat :impulse-backends:jolt:stageJoltNativeResource + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Java 25 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '25' + + - name: Set up Gradle cache + uses: gradle/actions/setup-gradle@v4 + + - name: Build Jolt native resource + shell: bash + run: ${{ matrix.gradle-command }} + + - name: Stage Jolt native artifact + shell: bash + run: | + artifact_dir="jolt-native-artifact/native/${{ matrix.resource-path }}" + mkdir -p "$artifact_dir" + cp "impulse-backends/jolt/build/generated/jolt-native/native/${{ matrix.resource-path }}/${{ matrix.library }}" "$artifact_dir/" + + - name: Upload Jolt native resource + uses: actions/upload-artifact@v4 + with: + name: jolt-native-${{ matrix.platform }} + path: jolt-native-artifact + if-no-files-found: error + rapier-native: name: Rapier native ${{ matrix.platform }} runs-on: ${{ matrix.runner }} @@ -20,25 +80,25 @@ jobs: rust-target: x86_64-unknown-linux-gnu resource-path: linux/x86_64 library: libimpulse_rapier.so - gradle-command: ./gradlew :impulse-rapier:stageRapierNativeResource + gradle-command: ./gradlew :impulse-backends:rapier:stageRapierNativeResource - platform: linux-arm64 runner: ubuntu-24.04-arm rust-target: aarch64-unknown-linux-gnu resource-path: linux/arm64 library: libimpulse_rapier.so - gradle-command: ./gradlew :impulse-rapier:stageRapierNativeResource + gradle-command: ./gradlew :impulse-backends:rapier:stageRapierNativeResource - platform: osx-arm64 runner: macos-15 rust-target: aarch64-apple-darwin resource-path: osx/arm64 library: libimpulse_rapier.dylib - gradle-command: ./gradlew :impulse-rapier:stageRapierNativeResource + gradle-command: ./gradlew :impulse-backends:rapier:stageRapierNativeResource - platform: windows-x86_64 runner: windows-2025 rust-target: x86_64-pc-windows-msvc resource-path: windows/x86_64 library: impulse_rapier.dll - gradle-command: ./gradlew.bat :impulse-rapier:stageRapierNativeResource + gradle-command: ./gradlew.bat :impulse-backends:rapier:stageRapierNativeResource steps: - name: Check out repository @@ -69,7 +129,7 @@ jobs: run: | artifact_dir="rapier-native-artifact/native/${{ matrix.resource-path }}" mkdir -p "$artifact_dir" - cp "impulse-rapier/build/generated/rapier-native/native/${{ matrix.resource-path }}/${{ matrix.library }}" "$artifact_dir/" + cp "impulse-backends/rapier/build/generated/rapier-native/native/${{ matrix.resource-path }}/${{ matrix.library }}" "$artifact_dir/" - name: Upload Rapier native resource uses: actions/upload-artifact@v4 @@ -81,7 +141,9 @@ jobs: backend-jars: name: Backend provider jars runs-on: ubuntu-24.04 - needs: rapier-native + needs: + - jolt-native + - rapier-native steps: - name: Check out repository @@ -100,26 +162,47 @@ jobs: uses: actions/download-artifact@v4 with: pattern: rapier-native-* - path: impulse-rapier/build/ci/rapier-native-downloads + path: impulse-backends/rapier/build/ci/rapier-native-downloads + + - name: Download Jolt native resources + uses: actions/download-artifact@v4 + with: + pattern: jolt-native-* + path: impulse-backends/jolt/build/ci/jolt-native-downloads - name: Normalize Rapier native resource tree shell: bash run: | - mkdir -p impulse-rapier/build/ci/rapier-native/native - for dir in impulse-rapier/build/ci/rapier-native-downloads/rapier-native-*; do + mkdir -p impulse-backends/rapier/build/ci/rapier-native/native + for dir in impulse-backends/rapier/build/ci/rapier-native-downloads/rapier-native-*; do if [ -d "$dir/native" ]; then - cp -R "$dir/native/." impulse-rapier/build/ci/rapier-native/native/ + cp -R "$dir/native/." impulse-backends/rapier/build/ci/rapier-native/native/ else - cp -R "$dir/." impulse-rapier/build/ci/rapier-native/native/ + cp -R "$dir/." impulse-backends/rapier/build/ci/rapier-native/native/ fi done - find impulse-rapier/build/ci/rapier-native/native -type f | sort + find impulse-backends/rapier/build/ci/rapier-native/native -type f | sort + + - name: Normalize Jolt native resource tree + shell: bash + run: | + mkdir -p impulse-backends/jolt/build/ci/jolt-native/native + for dir in impulse-backends/jolt/build/ci/jolt-native-downloads/jolt-native-*; do + if [ -d "$dir/native" ]; then + cp -R "$dir/native/." impulse-backends/jolt/build/ci/jolt-native/native/ + else + cp -R "$dir/." impulse-backends/jolt/build/ci/jolt-native/native/ + fi + done + find impulse-backends/jolt/build/ci/jolt-native/native -type f | sort - name: Build backend provider jars run: > ./gradlew packageBackendPlatformJars + -PbuildJoltNative=false + -Pimpulse.joltNativeResourceRoot=${{ github.workspace }}/impulse-backends/jolt/build/ci/jolt-native -PbuildRapierNative=false - -Pimpulse.rapierNativeResourceRoot=${{ github.workspace }}/impulse-rapier/build/ci/rapier-native + -Pimpulse.rapierNativeResourceRoot=${{ github.workspace }}/impulse-backends/rapier/build/ci/rapier-native - name: Write artifact notice shell: bash @@ -129,7 +212,9 @@ jobs: These backend provider jars may include third-party native binaries so Impulse can load the backend at runtime. - They are convenience packages for Impulse plugins and are not the official upstream distribution channel for those native libraries. Download standalone Bullet/Libbulletjme or Rapier binaries from their upstream projects instead. + They are convenience packages for Impulse plugins and are not the official upstream distribution channel for those native libraries. Download standalone Rapier binaries from its upstream project instead. + + Jolt backend jars include binaries built from upstream Jolt Physics source for Impulse's native ABI. See LICENSE and licenses/ in this artifact for packaged license notices. EOF @@ -141,16 +226,16 @@ jobs: path: | BACKEND_ARTIFACT_NOTICE.md LICENSE - licenses/LIBBULLETJME_LICENSE + licenses/JOLT_PHYSICS_LICENSE licenses/RAPIER_RUST_BACKEND_LICENSES - impulse-bullet/build/libs/impulse-bullet-*-linux-x86_64.jar - impulse-bullet/build/libs/impulse-bullet-*-linux-arm64.jar - impulse-bullet/build/libs/impulse-bullet-*-osx-arm64.jar - impulse-bullet/build/libs/impulse-bullet-*-windows-x86_64.jar - impulse-bullet/build/libs/impulse-bullet-*-universal.jar - impulse-rapier/build/libs/impulse-rapier-*-linux-x86_64.jar - impulse-rapier/build/libs/impulse-rapier-*-linux-arm64.jar - impulse-rapier/build/libs/impulse-rapier-*-osx-arm64.jar - impulse-rapier/build/libs/impulse-rapier-*-windows-x86_64.jar - impulse-rapier/build/libs/impulse-rapier-*-universal.jar + impulse-backends/jolt/build/libs/impulse-backend-jolt-*-linux-x86_64.jar + impulse-backends/jolt/build/libs/impulse-backend-jolt-*-linux-arm64.jar + impulse-backends/jolt/build/libs/impulse-backend-jolt-*-osx-arm64.jar + impulse-backends/jolt/build/libs/impulse-backend-jolt-*-windows-x86_64.jar + impulse-backends/jolt/build/libs/impulse-backend-jolt-*-universal.jar + impulse-backends/rapier/build/libs/impulse-backend-rapier-*-linux-x86_64.jar + impulse-backends/rapier/build/libs/impulse-backend-rapier-*-linux-arm64.jar + impulse-backends/rapier/build/libs/impulse-backend-rapier-*-osx-arm64.jar + impulse-backends/rapier/build/libs/impulse-backend-rapier-*-windows-x86_64.jar + impulse-backends/rapier/build/libs/impulse-backend-rapier-*-universal.jar if-no-files-found: error diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 61b1d8f4..3630226c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,8 @@ Open an issue or design discussion before changing architecture, persistence, li backend runtime contracts, public API packages or native artifact packaging. For bug reports, include a clear reproduction path and logs. When the bug depends on Hytale server -runtime behavior, also try to add or propose a failing Crucible test so that it can be reproduced in-game. +runtime behavior, also try to add or propose a focused JUnit/headless regression test, or document +the `runAllMods` world setup and commands needed to reproduce it. ## AI-Assisted Contributions @@ -67,8 +68,9 @@ Runtime/server behavior should be checked with: ./gradlew runAllMods ``` -When a runtime bug is fixed, prefer adding a focused Crucible test that reproduces the failing -server behavior. +When a runtime bug is fixed, prefer adding a focused module test that reproduces the failing +behavior. If the bug only exists in a live Hytale server loop, document the manual `runAllMods` +scenario used for validation. Backend-specific changes should name the backend used for validation, such as `impulse:bullet` or -`impulse:rapier`. \ No newline at end of file +`impulse:rapier`. diff --git a/README.md b/README.md index 6b4c1076..117fc0ab 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ Impulse is a physics framework for Hytale that connects Hytale ECS worlds to plu Impulse codebase is divided as follows: - **impulse-core** - Hytale ECS integration and backend communication. -- **impulse-api** - backend-agnostic API layer and contracts. -- **impulse-native-loader** - legacy loader for PhysicsBackend +- **impulse-backends/api** - backend-agnostic API layer and contracts. +- **impulse-backends/native-loader** - native library loader for backend provider jars. - **impulse-examples** - example plugins to understand the framework usage. Official physics backend implementations: -- **impulse-bullet** - Libbulletjme backend implementation. -- **impulse-rapier** - Rapier backend with a small Rust/JNI native shim. +- **impulse-backends/rapier** - Rapier backend with a small Rust/JNI native shim. +- **impulse-backends/jolt** - Jolt backend with a C++/Panama native shim. ### Architecture @@ -27,13 +27,12 @@ flowchart TB subgraph Examples["impulse-examples / plugin usage"] direction TB - Intents["Split ECS body components\nidentity / shape / dynamics / material / collision"] - ECSSystem["Body reconciliation systems"] - Direct["Direct command calls"] - Commands["command recipes / copied queries"] + Rows["PhysicsStore rows\nspaces / bodies / joints / terrain"] + Commands["row-local body commands + targets"] + Reads["copied snapshots / diagnostics / raycasts"] - Intents --> ECSSystem --> Commands - Direct --> Commands + Rows --> Commands + Commands --> Reads end subgraph CoreWorld["impulse-core: per-world runtime"] @@ -41,43 +40,36 @@ flowchart TB Plugin["Plugin API package"] - Modules["Internal modules\n- Hytale modules substitution (WIP)\n- World collision module\n- Control session module"] + Modules["Builtin subplugins\n- PhysicsEntity integration\n- PhysicsChunk terrain\n- Control sessions"] - Requests["physics command batches + query requests"] - Ordering["mutations + step request ordering"] - LaneBuild["owner-lane work units\ncomputed per world"] + StoreSystems["PhysicsStore systems + resources"] + Ordering["row mutation + backend step ordering"] + StoreTick["store tick lane\nper world"] - Plugin --> Requests - Modules --> Requests - Requests --> Ordering - Ordering --> LaneBuild + Plugin --> StoreSystems + Modules --> StoreSystems + StoreSystems --> Ordering + Ordering --> StoreTick end - subgraph OwnerQueues["owner-lane queues"] + subgraph StoreTicks["PhysicsStore ticks"] direction LR - QueueA["World A\nowner-lane queue"] - QueueB["World B\nowner-lane queue"] - QueueN["World N\nowner-lane queue"] + TickA["World A\nstore tick"] + TickB["World B\nstore tick"] + TickN["World N\nstore tick"] end - subgraph SharedCore["impulse-core: shared execution layer"] + subgraph SharedCore["impulse-core: backend dispatch"] direction TB - Scheduler["thread pool scheduler\nselects ready owner lanes"] - Workers["worker threads\nexecute owner lanes"] - Dispatch["backend dispatch"] - - Scheduler --> Workers --> Dispatch + Dispatch["serialized backend calls"] end - subgraph API["impulse-api"] + subgraph API["impulse-backends/api"] direction TB - Current["PhysicsBackend"] - WIP["Runtime / Provider"] - - Current ~~~ WIP + Runtime["PhysicsBackendRuntime"] end subgraph Backends["backends"] @@ -85,7 +77,7 @@ flowchart TB Java["Java engines"] Native["native engines"] - Bridge["impulse-native-bridge\nFFM (WIP) or JNI"] + Bridge["native loader\nFFM or JNI"] Active["active backend instances\nper physics space"] Native --> Bridge @@ -100,24 +92,22 @@ flowchart TB Router["snapshot + event publication"] end - Worlds --> Intents - Worlds --> Direct + Worlds --> Rows + Worlds --> Reads - Commands ----> Plugin + Reads ----> Plugin - LaneBuild --> QueueA - LaneBuild --> QueueB - LaneBuild --> QueueN + StoreTick --> TickA + StoreTick --> TickB + StoreTick --> TickN - QueueA ----> Scheduler - QueueB ----> Scheduler - QueueN ----> Scheduler + TickA ----> Dispatch + TickB ----> Dispatch + TickN ----> Dispatch - Dispatch ----> Current - Dispatch -.-> WIP + Dispatch ----> Runtime - Current ----> Active - WIP -.-> Active + Runtime ----> Active Active ----> Step Step --> Router @@ -176,7 +166,8 @@ You can start a debug server with all the example mods and backend jars by runni ### Backend Provider Jars -Backend jars are Java service-provider jars. Impulse discovers `PhysicsBackend` providers from jars anywhere under the configured Hytale `mods` directories. +Backend jars are Java service-provider jars. Impulse discovers `PhysicsBackendRuntimeProvider` +services from jars anywhere under the configured Hytale `mods` directories. When multiple backend jars are installed, create spaces with an explicit backend: @@ -184,10 +175,10 @@ When multiple backend jars are installed, create spaces with an explicit backend /impulse space create --backend=impulse:rapier ``` -The Rapier backend needs a Rust toolchain to build its native library. If `cargo` is available, `:impulse-rapier:processResources` builds and packages the current build platform native library automatically. You can also force native compilation with: +The Rapier backend needs a Rust toolchain to build its native library. If `cargo` is available, `:impulse-backends:rapier:processResources` builds and packages the current build platform native library automatically. You can also force native compilation with: ```bash -./gradlew :impulse-rapier:build -PbuildRapierNative=true +./gradlew :impulse-backends:rapier:build -PbuildRapierNative=true ``` It also supports SIMD optimizations that can be enabled using: @@ -204,10 +195,13 @@ Impulse has a dedicated headless/serverless test lane that does not boot the Hyt ./gradlew headlessTest ``` -Crucible in-game tests are also provided. Run them in game with: +For runtime/server behavior, use focused module tests first and then reproduce manually with the +Hytale runtime when the bug depends on plugin loading, live worlds, or command behavior: -``` -/crucible run +```bash +./gradlew :impulse-core:test +./gradlew :impulse-backends:rapier:test +./gradlew runAllMods ``` ## Native Binary Notice @@ -215,7 +209,7 @@ Crucible in-game tests are also provided. Run them in game with: Backend provider artifacts may include third-party native binaries so Impulse can load the backend at runtime. These artifacts are convenience packages for Impulse plugins; they are not the official upstream distribution channel for those native libraries. Download standalone -Bullet/Libbulletjme or Rapier binaries from their upstream projects instead. +Rapier binaries from their upstream project instead. ## Code style diff --git a/build.gradle.kts b/build.gradle.kts index a5b73db7..90b82f5a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,7 @@ import org.gradle.api.tasks.compile.JavaCompile import org.gradle.api.tasks.testing.Test import org.gradle.api.tasks.testing.logging.TestExceptionFormat import org.gradle.api.tasks.testing.logging.TestLogEvent +import org.gradle.process.CommandLineArgumentProvider plugins { alias(libs.plugins.hytale.workspace) @@ -16,13 +17,16 @@ version = property("version") as String val coreOnlyWorkspace = providers.gradleProperty("impulse.coreOnlyWorkspace") .map(String::toBoolean) .orElse(false) +val coreModProjects = listOf(":impulse-core") +val shadowedBuiltinProjects = listOf(":impulse-builtins:control") +val workspaceModProjects = if (coreOnlyWorkspace.get()) { + coreModProjects +} else { + listOf(":impulse-examples") + coreModProjects +} hytaleWorkspace { - modProjects = if (coreOnlyWorkspace.get()) { - listOf(":impulse-core") - } else { - listOf(":impulse-examples", ":impulse-core") - } + modProjects = workspaceModProjects hostProject = if (coreOnlyWorkspace.get()) { ":impulse-core" } else { @@ -64,11 +68,13 @@ subprojects { } } -val backendProjectPaths = setOf(":impulse-bullet", ":impulse-rapier") +val backendProjectPaths = setOf(":impulse-backends:jolt", ":impulse-backends:rapier") val stagedBackendJarDirectory = layout.projectDirectory.dir("run/mods/impulse-backends") -val hytaleToolProjectPaths = listOf( - ":impulse-core", - ":impulse-examples") +val stagedEarlyPluginJarDirectory = layout.projectDirectory.dir("run/earlyplugins") +val physicsStoreEarlyPluginEnabled = providers.gradleProperty("impulse.physicsStoreEarlyPlugin") + .map(String::toBoolean) + .orElse(true) +val hytaleToolProjectPaths = workspaceModProjects gradle.projectsEvaluated { val hytaleAssetDownloads = hytaleToolProjectPaths @@ -95,6 +101,12 @@ val cleanStagedBackendJars by tasks.registering(Delete::class) { delete(stagedBackendJarDirectory) } +val cleanStagedPhysicsStoreEarlyPluginJar by tasks.registering(Delete::class) { + delete(fileTree(stagedEarlyPluginJarDirectory) { + include("impulse-early-plugin-*.jar") + }) +} + val stageBackendJarsForRunAllMods by tasks.registering(Copy::class) { group = "hytale" description = "Stages backend provider jars beside Hytale mods for runAllMods" @@ -108,12 +120,26 @@ val stageBackendJarsForRunAllMods by tasks.registering(Copy::class) { into(stagedBackendJarDirectory) } +val stagePhysicsStoreEarlyPluginJar by tasks.registering(Copy::class) { + group = "hytale" + description = "Stages the PhysicsStore early plugin for runAllMods" + + onlyIf("PhysicsStore early plugin is enabled") { + physicsStoreEarlyPluginEnabled.get() + } + dependsOn(cleanStagedPhysicsStoreEarlyPluginJar) + val earlyJar = project(":impulse-early-plugin").tasks.named("jar") + dependsOn(earlyJar) + from(earlyJar) + into(stagedEarlyPluginJarDirectory) +} + tasks.register("packageBackendPlatformJars") { group = "build" description = "Packages all per-platform and universal backend provider jars" dependsOn( - ":impulse-bullet:packageBulletBackendPlatformJars", - ":impulse-rapier:packageRapierBackendPlatformJars" + ":impulse-backends:jolt:packageJoltBackendPlatformJars", + ":impulse-backends:rapier:packageRapierBackendPlatformJars" ) } @@ -121,11 +147,14 @@ tasks.register("headlessTest") { group = "verification" description = "Runs automated headless/serverless tests without booting the Hytale server" dependsOn( - ":impulse-api:test", - ":impulse-native-loader:test", - ":impulse-bullet:test", - ":impulse-rapier:test", - ":impulse-core:test" + ":impulse-backends:api:test", + ":impulse-backends:native-loader:test", + ":impulse-backends:jolt:test", + ":impulse-backends:rapier:test", + ":impulse-builtins:control:test", + ":impulse-core:test", + ":impulse-examples:test", + ":impulse-early-plugin:test" ) } @@ -133,16 +162,32 @@ tasks.register("headlessTest") { gradle.projectsEvaluated { tasks.named("runAllMods").configure { dependsOn(stageBackendJarsForRunAllMods) + if (physicsStoreEarlyPluginEnabled.get()) { + dependsOn(stagePhysicsStoreEarlyPluginJar) + } else { + dependsOn(cleanStagedPhysicsStoreEarlyPluginJar) + } val runTask = this as JavaExec runTask.standardInput = System.`in` - - // hytale-gradle 1.0.37 can omit project resources from run task classpaths. + runTask.jvmArgs("--enable-native-access=ALL-UNNAMED") + + // hytale-gradle can omit project resources from run task classpaths. + val toolRuntimeClasspaths = hytaleToolProjectPaths.map { path -> + val sourceSets = project(path).extensions.getByType() + sourceSets.named("main").get().runtimeClasspath + }.toMutableList() + shadowedBuiltinProjects.forEach { path -> + val sourceSets = project(path).extensions.getByType() + toolRuntimeClasspaths.add(sourceSets.named("main").get().runtimeClasspath) + } + if (physicsStoreEarlyPluginEnabled.get()) { + val sourceSets = project(":impulse-early-plugin") + .extensions.getByType() + toolRuntimeClasspaths.add(sourceSets.named("main").get().runtimeClasspath) + } runTask.classpath = files( - hytaleToolProjectPaths.map { path -> - val sourceSets = project(path).extensions.getByType() - sourceSets.named("main").get().runtimeClasspath - }, + toolRuntimeClasspaths, project(if (coreOnlyWorkspace.get()) ":impulse-core" else ":impulse-examples") .configurations.named("vineServerJar") ) @@ -152,5 +197,11 @@ gradle.projectsEvaluated { .map { args -> args.split(Regex("\\s+")).filter { it.isNotBlank() } } .orNull ?.let { runTask.jvmArgs(it) } + + if (physicsStoreEarlyPluginEnabled.get()) { + runTask.argumentProviders.add(CommandLineArgumentProvider { + listOf("--accept-early-plugins") + }) + } } } diff --git a/gradle.properties b/gradle.properties index 4ba736f4..29ae4418 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,7 +5,7 @@ org.gradle.parallel=false org.gradle.caching=false # Common java_version=25 -hytale_version=0.6.0-pre.3 +hytale_version=0.6.0-pre.4 release_type=PRE_RELEASE # Mod options group=dev.hytalemodding diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d4e59a58..4d334f93 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,6 @@ [versions] libbulletjme = "23.0.0" -crucible = "1.0.0" -hytale-gradle = "1.0.37" +hytale-gradle = "1.0.41" lombok = "1.18.38" joml = "1.10.5" jsr305 = "3.0.2" @@ -12,7 +11,6 @@ objenesis = "3.4" libbulletjme = { module = "com.github.stephengold:Libbulletjme-Linux64", version.ref = "libbulletjme" } # TODO: Linux and others... libbulletjme-native = { module = "com.github.stephengold:Libbulletjme-Linux64", version.ref = "libbulletjme" } -crucible = { module = "com.ionforgelabs:crucible", version.ref = "crucible" } lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" } joml = { module = "org.joml:joml", version.ref = "joml" } jsr305 = { module = "com.google.code.findbugs:jsr305", version.ref = "jsr305" } diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/Impulse.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/Impulse.java deleted file mode 100644 index 3a03635c..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/Impulse.java +++ /dev/null @@ -1,207 +0,0 @@ -package dev.hytalemodding.impulse.api; - -import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; -import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; -import dev.hytalemodding.impulse.api.runtime.legacy.LegacyPhysicsBackendRuntimeProvider; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.annotation.Nonnull; - -/** - * Entry point and backend registry for Impulse. - */ -public final class Impulse { - - private static final Logger LOGGER = Logger.getLogger("Impulse"); - - private static final Object REGISTRY_LOCK = new Object(); - - private static final Map BACKENDS = new HashMap<>(); - - private static final Map RUNTIME_PROVIDERS = - new HashMap<>(); - - private static final Map BACKEND_INIT_LOCKS = new HashMap<>(); - - private static final Set INITIALIZED_BACKENDS = new HashSet<>(); - - private Impulse() { - } - - /** - * Register or replace a backend implementation. - *

- * This method is thread-safe. - */ - @Deprecated(forRemoval = true) - public static void registerBackend(@Nonnull PhysicsBackend backend) { - synchronized (REGISTRY_LOCK) { - BACKENDS.put(backend.getId(), backend); - RUNTIME_PROVIDERS.putIfAbsent(backend.getId(), - new LegacyPhysicsBackendRuntimeProvider(backend)); - BACKEND_INIT_LOCKS.computeIfAbsent(backend.getId(), ignored -> new Object()); - INITIALIZED_BACKENDS.remove(backend.getId()); - } - } - - /** - * Register or replace an id-only backend runtime provider. - */ - public static void registerRuntimeProvider(@Nonnull PhysicsBackendRuntimeProvider provider) { - synchronized (REGISTRY_LOCK) { - RUNTIME_PROVIDERS.put(provider.getId(), provider); - BACKEND_INIT_LOCKS.computeIfAbsent(provider.getId(), ignored -> new Object()); - INITIALIZED_BACKENDS.remove(provider.getId()); - } - } - - @Nonnull - @Deprecated(forRemoval = true) - public static Collection getBackends() { - synchronized (REGISTRY_LOCK) { - return Collections.unmodifiableCollection(new ArrayList<>(BACKENDS.values())); - } - } - - @Nonnull - public static Collection getRuntimeProviders() { - synchronized (REGISTRY_LOCK) { - return Collections.unmodifiableCollection(new ArrayList<>(RUNTIME_PROVIDERS.values())); - } - } - - @Nonnull - @Deprecated(forRemoval = true) - public static PhysicsBackend getBackend(@Nonnull BackendId backendId) { - synchronized (REGISTRY_LOCK) { - PhysicsBackend backend = BACKENDS.get(backendId); - if (backend == null) { - throw new IllegalStateException("No backend registered with id: " + backendId); - } - return backend; - } - } - - @Nonnull - public static PhysicsBackendRuntimeProvider getRuntimeProvider(@Nonnull BackendId backendId) { - synchronized (REGISTRY_LOCK) { - PhysicsBackendRuntimeProvider provider = RUNTIME_PROVIDERS.get(backendId); - if (provider == null) { - throw new IllegalStateException("No backend runtime provider registered with id: " + backendId); - } - return provider; - } - } - - @Nonnull - public static PhysicsBackendRuntime createRuntime(@Nonnull BackendId backendId) { - PhysicsBackendRuntimeProvider provider = getRuntimeProvider(backendId); - ensureRuntimeProviderInitialized(backendId, provider); - return provider.createRuntime(); - } - - /** - * Create a space for the given backend id. - * - *

This method is safe to call concurrently after the backend is registered. The returned - * space is live backend state and must still be owned by one serialized physics owner lane.

- */ - @Nonnull - @Deprecated(forRemoval = true) - public static PhysicsSpace createSpace(@Nonnull BackendId backendId) { - return createSpace(backendId, SpaceId.next()); - } - - /** - * Create a space for the given backend id and logical space id. - * - *

This method is safe to call concurrently after the backend is registered. The returned - * space is live backend state and must still be owned by one serialized physics owner lane.

- */ - @Nonnull - @Deprecated(forRemoval = true) - public static PhysicsSpace createSpace(@Nonnull BackendId backendId, - @Nonnull SpaceId spaceId) { - PhysicsBackend backend = getBackend(backendId); - ensureBackendInitialized(backendId, backend); - PhysicsSpace space = backend.createSpace(spaceId); - if (!spaceId.equals(space.id())) { - throw new IllegalStateException("Backend " + backendId - + " created space id " + space.id() + " but expected " + spaceId); - } - return space; - } - - private static void ensureBackendInitialized(@Nonnull BackendId backendId, - @Nonnull PhysicsBackend backend) { - Object initLock; - synchronized (REGISTRY_LOCK) { - if (INITIALIZED_BACKENDS.contains(backendId)) { - LOGGER.log(Level.FINEST, - "Physics backend " + backendId + " already initialized"); - return; - } - initLock = BACKEND_INIT_LOCKS.computeIfAbsent(backendId, ignored -> new Object()); - } - - synchronized (initLock) { - synchronized (REGISTRY_LOCK) { - if (INITIALIZED_BACKENDS.contains(backendId)) { - LOGGER.log(Level.FINEST, - "Physics backend " + backendId + " already initialized"); - return; - } - } - - LOGGER.log(Level.FINE, - "Initializing physics backend " + backendId + " on thread " - + Thread.currentThread().getName()); - backend.init(); - - synchronized (REGISTRY_LOCK) { - INITIALIZED_BACKENDS.add(backendId); - } - LOGGER.log(Level.INFO, "Initialized physics backend " + backendId); - } - } - - private static void ensureRuntimeProviderInitialized(@Nonnull BackendId backendId, - @Nonnull PhysicsBackendRuntimeProvider provider) { - Object initLock; - synchronized (REGISTRY_LOCK) { - if (INITIALIZED_BACKENDS.contains(backendId)) { - LOGGER.log(Level.FINEST, - "Physics backend runtime " + backendId + " already initialized"); - return; - } - initLock = BACKEND_INIT_LOCKS.computeIfAbsent(backendId, ignored -> new Object()); - } - - synchronized (initLock) { - synchronized (REGISTRY_LOCK) { - if (INITIALIZED_BACKENDS.contains(backendId)) { - LOGGER.log(Level.FINEST, - "Physics backend runtime " + backendId + " already initialized"); - return; - } - } - - LOGGER.log(Level.FINE, - "Initializing physics backend runtime " + backendId + " on thread " - + Thread.currentThread().getName()); - provider.init(); - - synchronized (REGISTRY_LOCK) { - INITIALIZED_BACKENDS.add(backendId); - } - LOGGER.log(Level.INFO, "Initialized physics backend runtime " + backendId); - } - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackend.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackend.java deleted file mode 100644 index 12ac4acc..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackend.java +++ /dev/null @@ -1,63 +0,0 @@ -package dev.hytalemodding.impulse.api; - -import java.util.logging.Level; -import javax.annotation.Nonnull; - -/** - * Backend factory and lifecycle hooks. - *

- * This is a semantic contract, not a promise of identical simulation results. Backends may - * differ in solver details, contact ordering, and numerical edge cases. - *

    - *
  • {@link #init()} must be idempotent and safe when called multiple times.
  • - *
  • Backends are expected to be used through {@link Impulse}, which provides - * thread-safe one-time initialization.
  • - *
  • {@link #createSpace()} and {@link #createSpace(SpaceId)} may be called from multiple - * owner lanes after initialization. Implementations with mutable factory state must - * synchronize internally.
  • - *
  • Different spaces may run concurrently on different owner lanes. Each individual - * {@link PhysicsSpace} remains serialized by its own owner lane.
  • - *
- */ -@Deprecated(forRemoval = true) -public interface PhysicsBackend { - - @Nonnull - BackendId getId(); - - /** - * Set the verbosity of the backend's internal library logging. - * Backends should map this level to their own logging system. - * The default implementation is a no-op. - * - * @param level the desired logging level; Level.OFF suppresses all internal logging, - * Level.INFO allows standard output, Level.FINEST enables verbose diagnostics - */ - default void setInternalLoggingLevel(@Nonnull Level level) { - } - - /** - * Initialize backend-global state such as native libraries. - */ - void init(); - - /** - * Create a new independent simulation space. This method may be called concurrently after - * {@link #init()} has completed. - */ - @Nonnull - PhysicsSpace createSpace(); - - /** - * Create a new independent simulation space with a specific logical id. - *

- * This method may be called concurrently after {@link #init()} has completed. - * Implementations should preserve this id on the returned space object. - */ - @Nonnull - default PhysicsSpace createSpace(@Nonnull SpaceId spaceId) { - throw new UnsupportedOperationException( - "Legacy physics backend " + getId() + " must override createSpace(SpaceId) " - + "to support explicit space ids"); - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendBodyActivationEvent.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendBodyActivationEvent.java deleted file mode 100644 index 1f61757f..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendBodyActivationEvent.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.hytalemodding.impulse.api; - -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Copied body activation event emitted by a backend after a completed step. - */ -@Deprecated(forRemoval = true) -public record PhysicsBackendBodyActivationEvent(@Nonnull PhysicsBodyActivationPhase phase, - @Nonnull PhysicsBody body) implements PhysicsBackendEvent { - - public PhysicsBackendBodyActivationEvent { - phase = Objects.requireNonNull(phase, "phase"); - body = Objects.requireNonNull(body, "body"); - } - - @Nonnull - @Override - public PhysicsBackendEventKind kind() { - return phase.eventKind(); - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendContactEvent.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendContactEvent.java deleted file mode 100644 index 0a2ee642..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendContactEvent.java +++ /dev/null @@ -1,52 +0,0 @@ -package dev.hytalemodding.impulse.api; - -import java.util.Objects; -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -/** - * Copied contact event emitted by a backend after a completed step. - */ -@Deprecated(forRemoval = true) -public record PhysicsBackendContactEvent(@Nonnull PhysicsContactPhase phase, - @Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f pointOnA, - @Nonnull Vector3f pointOnB, - @Nonnull Vector3f normalOnB, - float distance, - float impulse) implements PhysicsBackendEvent { - - public PhysicsBackendContactEvent { - phase = Objects.requireNonNull(phase, "phase"); - bodyA = Objects.requireNonNull(bodyA, "bodyA"); - bodyB = Objects.requireNonNull(bodyB, "bodyB"); - pointOnA = new Vector3f(Objects.requireNonNull(pointOnA, "pointOnA")); - pointOnB = new Vector3f(Objects.requireNonNull(pointOnB, "pointOnB")); - normalOnB = new Vector3f(Objects.requireNonNull(normalOnB, "normalOnB")); - } - - @Nonnull - @Override - public PhysicsBackendEventKind kind() { - return phase.eventKind(); - } - - @Nonnull - @Override - public Vector3f pointOnA() { - return new Vector3f(pointOnA); - } - - @Nonnull - @Override - public Vector3f pointOnB() { - return new Vector3f(pointOnB); - } - - @Nonnull - @Override - public Vector3f normalOnB() { - return new Vector3f(normalOnB); - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEvent.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEvent.java deleted file mode 100644 index 32c89354..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEvent.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.hytalemodding.impulse.api; - -import javax.annotation.Nonnull; - -/** - * Copied backend event produced by a completed backend step. - */ -@Deprecated(forRemoval = true) -public interface PhysicsBackendEvent { - - @Nonnull - PhysicsBackendEventKind kind(); -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEventBatch.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEventBatch.java deleted file mode 100644 index 45cd0fdb..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEventBatch.java +++ /dev/null @@ -1,33 +0,0 @@ -package dev.hytalemodding.impulse.api; - -import java.util.List; -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Immutable post-step backend event batch drained by core after a backend step completes. - */ -@Deprecated(forRemoval = true) -public record PhysicsBackendEventBatch(@Nonnull List events, - int droppedEventCount) { - - private static final PhysicsBackendEventBatch EMPTY = new PhysicsBackendEventBatch(List.of(), 0); - - public PhysicsBackendEventBatch { - events = List.copyOf(Objects.requireNonNull(events, "events")); - droppedEventCount = Math.max(0, droppedEventCount); - } - - @Nonnull - public static PhysicsBackendEventBatch empty() { - return EMPTY; - } - - public int size() { - return events.size(); - } - - public boolean isEmpty() { - return events.isEmpty() && droppedEventCount == 0; - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEventBuffer.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEventBuffer.java deleted file mode 100644 index 46cf428d..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEventBuffer.java +++ /dev/null @@ -1,70 +0,0 @@ -package dev.hytalemodding.impulse.api; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Bounded in-memory backend event buffer for one or more completed backend steps. - */ -@Deprecated(forRemoval = true) -public final class PhysicsBackendEventBuffer implements PhysicsBackendEventSink { - - public static final int DEFAULT_CAPACITY = 1024; - - private final int capacity; - private final ArrayList events; - private int droppedEventCount; - - public PhysicsBackendEventBuffer() { - this(DEFAULT_CAPACITY); - } - - public PhysicsBackendEventBuffer(int capacity) { - this.capacity = Math.max(0, capacity); - this.events = new ArrayList<>(Math.min(this.capacity, DEFAULT_CAPACITY)); - } - - @Override - public int capacity() { - return capacity; - } - - @Override - public int size() { - return events.size(); - } - - @Override - public int droppedEventCount() { - return droppedEventCount; - } - - @Override - public boolean offer(@Nonnull PhysicsBackendEvent event) { - Objects.requireNonNull(event, "event"); - if (events.size() >= capacity) { - droppedEventCount++; - return false; - } - events.add(event); - return true; - } - - @Nonnull - public PhysicsBackendEventBatch drain() { - if (events.isEmpty() && droppedEventCount == 0) { - return PhysicsBackendEventBatch.empty(); - } - PhysicsBackendEventBatch batch = - new PhysicsBackendEventBatch(List.copyOf(events), droppedEventCount); - clear(); - return batch; - } - - public void clear() { - events.clear(); - droppedEventCount = 0; - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEventSink.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEventSink.java deleted file mode 100644 index cad977d2..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEventSink.java +++ /dev/null @@ -1,46 +0,0 @@ -package dev.hytalemodding.impulse.api; - -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -/** - * Sink for copied backend events emitted into a bounded post-step batch. - */ -@Deprecated(forRemoval = true) -public interface PhysicsBackendEventSink { - - int capacity(); - - int size(); - - int droppedEventCount(); - - boolean offer(@Nonnull PhysicsBackendEvent event); - - default boolean contact(@Nonnull PhysicsContactPhase phase, - @Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f pointOnA, - @Nonnull Vector3f pointOnB, - @Nonnull Vector3f normalOnB, - float distance, - float impulse) { - return offer(new PhysicsBackendContactEvent(phase, - bodyA, - bodyB, - pointOnA, - pointOnB, - normalOnB, - distance, - impulse)); - } - - default boolean bodyActivation(@Nonnull PhysicsBodyActivationPhase phase, - @Nonnull PhysicsBody body) { - return offer(new PhysicsBackendBodyActivationEvent(phase, body)); - } - - default boolean jointBreak(@Nonnull PhysicsJoint joint) { - return offer(new PhysicsBackendJointBreakEvent(joint)); - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendJointBreakEvent.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendJointBreakEvent.java deleted file mode 100644 index 305999b8..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendJointBreakEvent.java +++ /dev/null @@ -1,21 +0,0 @@ -package dev.hytalemodding.impulse.api; - -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Copied joint-break event emitted by a backend after a completed step. - */ -@Deprecated(forRemoval = true) -public record PhysicsBackendJointBreakEvent(@Nonnull PhysicsJoint joint) implements PhysicsBackendEvent { - - public PhysicsBackendJointBreakEvent { - joint = Objects.requireNonNull(joint, "joint"); - } - - @Nonnull - @Override - public PhysicsBackendEventKind kind() { - return PhysicsBackendEventKind.JOINT_BROKEN; - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBody.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBody.java deleted file mode 100644 index 6c1989b1..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBody.java +++ /dev/null @@ -1,224 +0,0 @@ -package dev.hytalemodding.impulse.api; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -/** - * Backend-agnostic rigid body facade. - * TODO: rigid body for now, soft body and particles need to be addressed - *

- * The API defines shared behavior for different backends, but backends may still differ in - * solver details, contact reporting, and exact motion response. - */ -@Deprecated(forRemoval = true) -public interface PhysicsBody { - - void setPosition(float x, float y, float z); - - void setPosition(@Nonnull Vector3f pos); - - @Nonnull - Vector3f getPosition(); - - /** - * Copy the current world-space center of mass into the provided vector. - * Backends should override this to avoid allocating on hot sync paths. - */ - default void getPosition(@Nonnull Vector3f out) { - out.set(getPosition()); - } - - void setRotation(float x, float y, float z, float w); - - void setRotation(@Nonnull Quaternionf rot); - - @Nonnull - Quaternionf getRotation(); - - /** - * Copy the current world-space rotation into the provided quaternion. - * Backends should override this to avoid allocating on hot sync paths. - */ - default void getRotation(@Nonnull Quaternionf out) { - out.set(getRotation()); - } - - void setRestitution(float restitution); - - float getRestitution(); - - void setFriction(float friction); - - float getFriction(); - - @Nonnull - PhysicsBodyType getBodyType(); - - void setBodyType(@Nonnull PhysicsBodyType bodyType); - - boolean isStatic(); - - default boolean isDynamic() { - return getBodyType() == PhysicsBodyType.DYNAMIC; - } - - boolean isKinematic(); - - void setKinematic(boolean kinematic); - - void activate(); - - boolean isActive(); - - boolean isSleeping(); - - void sleep(); - - float getMass(); - - void setMass(float mass); - - @Nonnull - Vector3f getLinearVelocity(); - - /** - * Copy the current linear velocity into the provided vector. - */ - default void getLinearVelocity(@Nonnull Vector3f out) { - out.set(getLinearVelocity()); - } - - void setLinearVelocity(@Nonnull Vector3f vel); - - void setLinearVelocity(float x, float y, float z); - - @Nonnull - Vector3f getAngularVelocity(); - - /** - * Copy the current angular velocity into the provided vector. - */ - default void getAngularVelocity(@Nonnull Vector3f out) { - out.set(getAngularVelocity()); - } - - void setAngularVelocity(@Nonnull Vector3f vel); - - void setAngularVelocity(float x, float y, float z); - - float getLinearDamping(); - - void setLinearDamping(float damping); - - float getAngularDamping(); - - void setAngularDamping(float damping); - - default void setDamping(float linearDamping, float angularDamping) { - setLinearDamping(linearDamping); - setAngularDamping(angularDamping); - } - - void applyCentralForce(@Nonnull Vector3f force); - - void applyCentralForce(float x, float y, float z); - - void applyForce(@Nonnull Vector3f force, @Nonnull Vector3f offset); - - /** - * Applies force at a world-space offset without requiring caller-side vector allocation. - * Backends should override this to avoid the default temporary vector fallback. - */ - default void applyForce(float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ) { - applyForce(new Vector3f(x, y, z), new Vector3f(offsetX, offsetY, offsetZ)); - } - - void applyCentralImpulse(@Nonnull Vector3f impulse); - - void applyCentralImpulse(float x, float y, float z); - - void applyImpulse(@Nonnull Vector3f impulse, @Nonnull Vector3f offset); - - /** - * Applies impulse at a world-space offset without requiring caller-side vector allocation. - * Backends should override this to avoid the default temporary vector fallback. - */ - default void applyImpulse(float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ) { - applyImpulse(new Vector3f(x, y, z), new Vector3f(offsetX, offsetY, offsetZ)); - } - - void applyTorque(@Nonnull Vector3f torque); - - /** - * Applies torque without requiring caller-side vector allocation. - * Backends should override this to avoid the default temporary vector fallback. - */ - default void applyTorque(float x, float y, float z) { - applyTorque(new Vector3f(x, y, z)); - } - - void applyTorqueImpulse(@Nonnull Vector3f torqueImpulse); - - /** - * Applies torque impulse without requiring caller-side vector allocation. - * Backends should override this to avoid the default temporary vector fallback. - */ - default void applyTorqueImpulse(float x, float y, float z) { - applyTorqueImpulse(new Vector3f(x, y, z)); - } - - void clearForces(); - - /** - * Returns whether this body is configured as a sensor/trigger. - *

- * Sensor bodies participate in overlap/contact callbacks but do not produce - * normal physical contact response or collision resolution. - */ - boolean isSensor(); - - /** - * Sets whether this body should behave as a sensor/trigger. - *

- * When enabled, the body can overlap other bodies without acting as a solid - * collider. - */ - void setSensor(boolean sensor); - - int getCollisionGroup(); - - int getCollisionMask(); - - void setCollisionFilter(int group, int mask); - - boolean isContinuousCollisionEnabled(); - - void setContinuousCollisionEnabled(boolean enabled); - - @Nonnull - ShapeType getShapeType(); - - @Nullable - Vector3f getBoxHalfExtents(); - - float getSphereRadius(); - - float getHalfHeight(); - - @Nonnull - PhysicsAxis getShapeAxis(); - - float getCenterOfMassOffsetY(); -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsContact.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsContact.java deleted file mode 100644 index dcaae11d..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsContact.java +++ /dev/null @@ -1,38 +0,0 @@ -package dev.hytalemodding.impulse.api; - -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -@Deprecated(forRemoval = true) -public record PhysicsContact(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f pointOnA, - @Nonnull Vector3f pointOnB, - @Nonnull Vector3f normalOnB, - float distance, - float impulse) { - - public PhysicsContact { - pointOnA = new Vector3f(pointOnA); - pointOnB = new Vector3f(pointOnB); - normalOnB = new Vector3f(normalOnB); - } - - @Nonnull - @Override - public Vector3f pointOnA() { - return new Vector3f(pointOnA); - } - - @Nonnull - @Override - public Vector3f pointOnB() { - return new Vector3f(pointOnB); - } - - @Nonnull - @Override - public Vector3f normalOnB() { - return new Vector3f(normalOnB); - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsJoint.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsJoint.java deleted file mode 100644 index eefe396f..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsJoint.java +++ /dev/null @@ -1,108 +0,0 @@ -package dev.hytalemodding.impulse.api; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3f; - -/** - * A live joint constraint between two bodies. - * Anchors are local to each body. - * Axis is only meaningful for hinge and slider joints. - * Limits and motor controls are mainly for hinge and slider joints. - */ -@Deprecated(forRemoval = true) -public interface PhysicsJoint { - - @Nonnull - PhysicsJointType getType(); - - @Nonnull - PhysicsBody getBodyA(); - - @Nonnull - PhysicsBody getBodyB(); - - boolean isEnabled(); - - void setEnabled(boolean enabled); - - @Nonnull - Vector3f getAnchorA(); - - @Nonnull - Vector3f getAnchorB(); - - /** - * Return the joint axis when the joint type uses one. - * Fixed, point, and spring joints may return null. - */ - @Nullable - Vector3f getAxis(); - - /** - * Return the lower joint limit when the joint supports limits. - * Hinge and slider joints use this value. - */ - float getLowerLimit(); - - /** - * Return the upper joint limit when the joint supports limits. - * Hinge and slider joints use this value. - */ - float getUpperLimit(); - - /** - * Set the joint limits when the joint supports them. - * Backends may ignore this for joints that do not use limits. - */ - void setLimits(float lowerLimit, float upperLimit); - - /** - * Return whether the motor is enabled for joints that support motors. - */ - boolean isMotorEnabled(); - - /** - * Enable or disable the motor when the joint supports motors. - */ - void setMotorEnabled(boolean enabled); - - /** - * Return the configured motor target velocity. - */ - float getMotorTargetVelocity(); - - /** - * Return the configured motor max force. - */ - float getMotorMaxForce(); - - /** - * Configure the joint motor when the joint supports motors. - */ - void setMotor(float targetVelocity, float maxForce); - - /** - * Return configured spring rest length for spring joints. - * Returns {@link Float#NaN} for non-spring joints. - */ - default float getSpringRestLength() { - return Float.NaN; - } - - /** - * Return configured spring stiffness for spring joints. - * Returns {@link Float#NaN} for non-spring joints. - */ - default float getSpringStiffness() { - return Float.NaN; - } - - /** - * Return configured spring damping for spring joints. - * Returns {@link Float#NaN} for non-spring joints. - */ - default float getSpringDamping() { - return Float.NaN; - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsJointType.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsJointType.java deleted file mode 100644 index aa549cfb..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsJointType.java +++ /dev/null @@ -1,32 +0,0 @@ -package dev.hytalemodding.impulse.api; - -/** - * Joint kinds supported by the shared API. - */ -public enum PhysicsJointType { - - /** - * Locks two bodies together - */ - FIXED, - - /** - * Keeps one anchor between the bodies and allows free rotation - */ - POINT, - - /** - * Rotation around one axis - */ - HINGE, - - /** - * Movement along a direction/axis - */ - SLIDER, - - /** - * Spring behavior between two bodies - */ - SPRING -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsRayHit.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsRayHit.java deleted file mode 100644 index a474f8be..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsRayHit.java +++ /dev/null @@ -1,29 +0,0 @@ -package dev.hytalemodding.impulse.api; - -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -@Deprecated(forRemoval = true) -public record PhysicsRayHit(@Nonnull PhysicsBody body, - @Nonnull Vector3f point, - @Nonnull Vector3f normal, - float fraction, - float distance) { - - public PhysicsRayHit { - point = new Vector3f(point); - normal = new Vector3f(normal); - } - - @Nonnull - @Override - public Vector3f point() { - return new Vector3f(point); - } - - @Nonnull - @Override - public Vector3f normal() { - return new Vector3f(normal); - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsSpace.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsSpace.java deleted file mode 100644 index 1f1365d0..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsSpace.java +++ /dev/null @@ -1,420 +0,0 @@ -package dev.hytalemodding.impulse.api; - -import dev.hytalemodding.impulse.api.capability.PhysicsCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsCapabilityDescriptor; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -/** - * Live simulation container for a physics backend. - *

- * One world can have different physics spaces of different space size. - *

- *

- * Backends are expected to agree on the meaning of these operations, but not on identical - * numerical output. Contact ordering, solver settling, and ray hit details can differ slightly - * between implementations. - *

    - *
  • This type must not be assumed to be thread-safe.
  • - *
  • All mutations, stepping, and live snapshots must happen from a single serialized owner - * lane.
  • - *
  • The owner lane is a logical execution context, not a public Java thread identity. It may - * be backed by pooled executor lanes, but it must not execute the same space concurrently.
  • - *
  • If other threads need to interact, queue commands onto the owner lane.
  • - *
- */ -@Deprecated(forRemoval = true) -public interface PhysicsSpace { - - @Nonnull - SpaceId id(); - - @Nonnull - BackendId backendId(); - - void step(float dt); - - default void step(float dt, @Nonnull PhysicsBackendEventSink events) { - step(dt); - } - - void setGravity(float x, float y, float z); - - @Nonnull - Vector3f getGravity(); - - void addBody(@Nonnull PhysicsBody body); - - void removeBody(@Nonnull PhysicsBody body); - - @Nonnull - List getBodies(); - - /** - * Returns the number of bodies in this space. - * - *

Default implementation delegates to {@link #getBodies()}. - * Backends should override to avoid list allocation.

- */ - default int bodyCount() { - return getBodies().size(); - } - - /** - * Iterates all bodies without allocating a copied list. - * - *

Default implementation delegates to {@link #getBodies()}. - * Backends should override to iterate internal lists directly.

- */ - default void forEachBody(@Nonnull Consumer consumer) { - for (PhysicsBody body : getBodies()) { - consumer.accept(body); - } - } - - /** - * Returns whether the given body is currently attached to this space. - * - *

Default implementation delegates to {@link #getBodies()}. - * Backends should override to avoid list allocation.

- */ - default boolean containsBody(@Nonnull PhysicsBody body) { - return getBodies().contains(body); - } - - /** - * Publishes owner-lane body snapshots for systems that must not repeatedly - * read mutable backend bodies. - */ - default void snapshotBodies(@Nonnull Consumer consumer) { - snapshotBodies(body -> null, consumer); - } - - /** - * Publishes owner-lane body snapshots, allowing callers to provide the last - * published snapshot so backends can avoid stable sleeping-body refreshes. - */ - default void snapshotBodies(@Nonnull Function previousSnapshots, - @Nonnull Consumer consumer) { - snapshotBodies(previousSnapshots, (_, snapshot) -> consumer.accept(snapshot)); - } - - /** - * Publishes owner-lane body snapshots with the live body available only during the callback. - */ - default void snapshotBodies(@Nonnull Function previousSnapshots, - @Nonnull BiConsumer consumer) { - forEachBody(body -> consumer.accept(body, PhysicsBodySnapshot.from(body, previousSnapshots.apply(body)))); - } - - /** - * Publishes owner-lane snapshots for a caller-selected subset of bodies. - * - *

This lets backends batch-read only bodies that higher-level systems actually - * need to publish. Callers should pass bodies that currently belong to this space.

- */ - default void snapshotBodies(@Nonnull Iterable selectedBodies, - @Nonnull Function previousSnapshots, - @Nonnull Consumer consumer) { - snapshotBodies(selectedBodies, previousSnapshots, (_, snapshot) -> consumer.accept(snapshot)); - } - - /** - * Publishes owner-lane snapshots for a caller-selected subset of bodies with the live body - * available only during the callback. - */ - default void snapshotBodies(@Nonnull Iterable selectedBodies, - @Nonnull Function previousSnapshots, - @Nonnull BiConsumer consumer) { - for (PhysicsBody body : selectedBodies) { - consumer.accept(body, PhysicsBodySnapshot.from(body, previousSnapshots.apply(body))); - } - } - - /** - * Returns optional backend runtime counters for diagnostics. - * - *

The default implementation reports no backend-specific runtime counters so - * existing backends do not need to synthesize opaque internal state.

- */ - @Nonnull - default PhysicsRuntimeStats getRuntimeStats() { - return PhysicsRuntimeStats.unavailable(); - } - - /** - * Resets backend-native phase counters collected by {@link #getStepPhaseStats()}. - * - *

Backends that do not expose phase timings may keep the default no-op behavior.

- */ - default void resetStepPhaseStats() { - } - - /** - * Returns backend-native phase timings collected since the last reset. - * - *

These counters are intended for profiling only. Unsupported backends should return - * {@link PhysicsStepPhaseStats#unavailable()}.

- */ - @Nonnull - default PhysicsStepPhaseStats getStepPhaseStats() { - return PhysicsStepPhaseStats.unavailable(); - } - - /** - * Returns an optional backend-specific capability for this space. - * - *

The default implementation supports spaces that directly implement a capability - * interface. Backends that expose separate capability objects should override this method.

- */ - @Nonnull - default Optional getCapability(@Nonnull Class type) { - Objects.requireNonNull(type, "type"); - if (type.isInstance(this)) { - return Optional.of(type.cast(this)); - } - return Optional.empty(); - } - - /** - * Returns metadata for backend-specific capabilities exposed by this space. - */ - @Nonnull - default List getCapabilityDescriptors() { - return List.of(); - } - - @Nonnull - PhysicsBody createStaticPlane(float groundY); - - @Nonnull - PhysicsBody createBox(float halfX, float halfY, float halfZ, float mass); - - @Nonnull - PhysicsBody createBox(@Nonnull Vector3f halfExtents, float mass); - - @Nonnull - PhysicsBody createSphere(float radius, float mass); - - @Nonnull - PhysicsBody createCapsule(float radius, float halfHeight, @Nonnull PhysicsAxis axis, - float mass); - - @Nonnull - PhysicsBody createCylinder(float radius, float halfHeight, @Nonnull PhysicsAxis axis, - float mass); - - @Nonnull - PhysicsBody createCone(float radius, float halfHeight, @Nonnull PhysicsAxis axis, float mass); - - @Nonnull - Optional raycastClosest(@Nonnull Vector3f from, @Nonnull Vector3f to); - - @Nonnull - List raycastAll(@Nonnull Vector3f from, @Nonnull Vector3f to); - - @Nonnull - List getContacts(); - - /** - * Returns the number of active contacts in this space. - * - *

Default implementation delegates to {@link #getContacts()}. - * Backends should override to avoid contact-list allocation.

- */ - default int contactCount() { - return getContacts().size(); - } - - /** - * Create a fixed joint. - * Anchors are local to each body. - * The joint locks the bodies together with no relative translation or rotation. - */ - @Nonnull - PhysicsJoint createFixedJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB); - - @Nonnull - default PhysicsJoint createFixedJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ) { - return createFixedJoint(bodyA, - bodyB, - new Vector3f(anchorAX, anchorAY, anchorAZ), - new Vector3f(anchorBX, anchorBY, anchorBZ)); - } - - /** - * Create a point joint. - * Anchors are local to each body. - * The joint keeps the two anchors together but allows free rotation. - */ - @Nonnull - PhysicsJoint createPointJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB); - - @Nonnull - default PhysicsJoint createPointJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ) { - return createPointJoint(bodyA, - bodyB, - new Vector3f(anchorAX, anchorAY, anchorAZ), - new Vector3f(anchorBX, anchorBY, anchorBZ)); - } - - /** - * Create a hinge joint. - * Anchors are local to each body. - * Axis describes the hinge axis in joint local space. - * The joint allows rotation around that axis. - */ - @Nonnull - PhysicsJoint createHingeJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis); - - @Nonnull - default PhysicsJoint createHingeJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ) { - return createHingeJoint(bodyA, - bodyB, - new Vector3f(anchorAX, anchorAY, anchorAZ), - new Vector3f(anchorBX, anchorBY, anchorBZ), - new Vector3f(axisX, axisY, axisZ)); - } - - /** - * Create a slider joint. - * Anchors are local to each body. - * Axis describes the slide axis in joint local space. - * The joint allows translation along that axis. - */ - @Nonnull - PhysicsJoint createSliderJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis); - - @Nonnull - default PhysicsJoint createSliderJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ) { - return createSliderJoint(bodyA, - bodyB, - new Vector3f(anchorAX, anchorAY, anchorAZ), - new Vector3f(anchorBX, anchorBY, anchorBZ), - new Vector3f(axisX, axisY, axisZ)); - } - - /** - * Create a spring joint. - * Anchors are local to each body. - * Rest length, stiffness, and damping define the spring behavior. - */ - @Nonnull - PhysicsJoint createSpringJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - float restLength, - float stiffness, - float damping); - - @Nonnull - default PhysicsJoint createSpringJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float restLength, - float stiffness, - float damping) { - return createSpringJoint(bodyA, - bodyB, - new Vector3f(anchorAX, anchorAY, anchorAZ), - new Vector3f(anchorBX, anchorBY, anchorBZ), - restLength, - stiffness, - damping); - } - - void removeJoint(@Nonnull PhysicsJoint joint); - - @Nonnull - List getJoints(); - - /** - * Returns the number of joints in this space. - * - *

Default implementation delegates to {@link #getJoints()}. - * Backends should override to avoid list allocation.

- */ - default int jointCount() { - return getJoints().size(); - } - - /** - * Iterates all joints without allocating a copied list. - * - *

Default implementation delegates to {@link #getJoints()}. - * Backends should override to iterate internal lists directly.

- */ - default void forEachJoint(@Nonnull Consumer consumer) { - for (PhysicsJoint joint : getJoints()) { - consumer.accept(joint); - } - } - - /** - * Release backend resources for this space. - *

- * Default implementation is a no-op. - */ - default void close() { - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsActivationTuningCapability.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsActivationTuningCapability.java deleted file mode 100644 index 73266af1..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsActivationTuningCapability.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.hytalemodding.impulse.api.capability; - -import javax.annotation.Nonnull; - -/** - * Optional backend capability for tuning dynamic body sleep behavior. - */ -@Deprecated(forRemoval = true) -public interface PhysicsActivationTuningCapability extends PhysicsCapability { - - PhysicsCapabilityDescriptor DESCRIPTOR = new PhysicsCapabilityDescriptor( - new PhysicsCapabilityId("impulse:activation_tuning"), - "Activation tuning", - "Configures dynamic body sleep thresholds"); - - void setActivationTuning(@Nonnull PhysicsActivationTuning tuning); -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsBackendEventsCapability.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsBackendEventsCapability.java deleted file mode 100644 index 75340cae..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsBackendEventsCapability.java +++ /dev/null @@ -1,29 +0,0 @@ -package dev.hytalemodding.impulse.api.capability; - -import dev.hytalemodding.impulse.api.PhysicsBackendEventKind; -import dev.hytalemodding.impulse.api.PhysicsContactPhase; -import java.util.Set; -import javax.annotation.Nonnull; - -/** - * Optional backend capability indicating which post-step backend events may be emitted. - */ -@Deprecated(forRemoval = true) -public interface PhysicsBackendEventsCapability extends PhysicsCapability { - - PhysicsCapabilityDescriptor DESCRIPTOR = new PhysicsCapabilityDescriptor( - new PhysicsCapabilityId("impulse:backend_events"), - "Backend events", - "Reports backend event kinds emitted during physics steps"); - - @Nonnull - Set supportedEventKinds(); - - default boolean supportsEventKind(@Nonnull PhysicsBackendEventKind kind) { - return supportedEventKinds().contains(kind); - } - - default boolean supportsContactPhase(@Nonnull PhysicsContactPhase phase) { - return supportsEventKind(phase.eventKind()); - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsCapability.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsCapability.java deleted file mode 100644 index 3861c68d..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsCapability.java +++ /dev/null @@ -1,8 +0,0 @@ -package dev.hytalemodding.impulse.api.capability; - -/** - * Marker for optional backend capabilities exposed by a physics space. - */ -@Deprecated(forRemoval = true) -public interface PhysicsCapability { -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilityDescriptor.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilityDescriptor.java deleted file mode 100644 index f454dd6e..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilityDescriptor.java +++ /dev/null @@ -1,28 +0,0 @@ -package dev.hytalemodding.impulse.api.capability; - -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Public metadata for an optional backend capability. - * - * @param id stable capability identifier - * @param displayName concise user-facing name - * @param description concise capability description - */ -public record PhysicsCapabilityDescriptor(@Nonnull PhysicsCapabilityId id, - @Nonnull String displayName, - @Nonnull String description) { - - public PhysicsCapabilityDescriptor { - Objects.requireNonNull(id, "id"); - Objects.requireNonNull(displayName, "displayName"); - Objects.requireNonNull(description, "description"); - if (displayName.isBlank()) { - throw new IllegalArgumentException("displayName cannot be blank"); - } - if (description.isBlank()) { - throw new IllegalArgumentException("description cannot be blank"); - } - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsContinuousCollisionCapability.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsContinuousCollisionCapability.java deleted file mode 100644 index 9e56f85f..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsContinuousCollisionCapability.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.hytalemodding.impulse.api.capability; - -/** - * Optional backend capability indicating support for continuous collision detection. - */ -@Deprecated(forRemoval = true) -public interface PhysicsContinuousCollisionCapability extends PhysicsCapability { - - PhysicsCapabilityDescriptor DESCRIPTOR = new PhysicsCapabilityDescriptor( - new PhysicsCapabilityId("impulse:continuous_collision"), - "Continuous collision", - "Supports continuous collision detection on bodies"); - - default boolean supportsContinuousCollision() { - return true; - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsExtensionSettingsCapability.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsExtensionSettingsCapability.java deleted file mode 100644 index 98ea6e71..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsExtensionSettingsCapability.java +++ /dev/null @@ -1,22 +0,0 @@ -package dev.hytalemodding.impulse.api.capability; - -import java.util.Map; -import javax.annotation.Nonnull; - -/** - * Optional backend capability for applying capability-keyed extension settings. - * - *

The setting values are strings so core can persist and forward unknown backend - * settings without depending on backend-owned Java types.

- */ -@Deprecated(forRemoval = true) -public interface PhysicsExtensionSettingsCapability extends PhysicsCapability { - - PhysicsCapabilityDescriptor DESCRIPTOR = new PhysicsCapabilityDescriptor( - new PhysicsCapabilityId("impulse:extension_settings"), - "Extension settings", - "Applies capability-keyed backend extension settings"); - - void applyExtensionSettings(@Nonnull PhysicsCapabilityId capabilityId, - @Nonnull Map values); -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsSolverTuningCapability.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsSolverTuningCapability.java deleted file mode 100644 index 3bb80975..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsSolverTuningCapability.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.hytalemodding.impulse.api.capability; - -import javax.annotation.Nonnull; - -/** - * Optional backend capability for tuning solver cost versus stability. - */ -@Deprecated(forRemoval = true) -public interface PhysicsSolverTuningCapability extends PhysicsCapability { - - PhysicsCapabilityDescriptor DESCRIPTOR = new PhysicsCapabilityDescriptor( - new PhysicsCapabilityId("impulse:solver_tuning"), - "Solver tuning", - "Configures solver and stabilization iterations"); - - void setSolverTuning(@Nonnull PhysicsSolverTuning tuning); -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsVoxelTerrainCapability.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsVoxelTerrainCapability.java deleted file mode 100644 index d2d44c55..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsVoxelTerrainCapability.java +++ /dev/null @@ -1,44 +0,0 @@ -package dev.hytalemodding.impulse.api.capability; - -import dev.hytalemodding.impulse.api.PhysicsBody; -import javax.annotation.Nonnull; - -/** - * Optional backend capability for native voxel terrain collision. - */ -@Deprecated(forRemoval = true) -public interface PhysicsVoxelTerrainCapability extends PhysicsCapability { - - PhysicsCapabilityDescriptor DESCRIPTOR = new PhysicsCapabilityDescriptor( - new PhysicsCapabilityId("impulse:voxel_terrain"), - "Voxel terrain", - "Creates native static voxel terrain bodies"); - - /** - * Creates a static terrain body made from occupied voxel cells. - * - *

The {@code voxelCoordinates} array stores triples of local integer grid coordinates: - * {@code x0, y0, z0, x1, y1, z1, ...}. The returned body can then be positioned at the - * section/world origin like any other static body.

- */ - @Nonnull - PhysicsBody createVoxelTerrain(float voxelSizeX, - float voxelSizeY, - float voxelSizeZ, - @Nonnull int[] voxelCoordinates); - - /** - * Couples two adjacent voxel terrain bodies so the backend can treat their shared boundary - * as continuous terrain instead of two unrelated voxel sets. - * - *

The shift is expressed in voxel units from {@code bodyA}'s local voxel origin to - * {@code bodyB}'s local voxel origin. Backends that do not use native adjacency hints can keep - * the default no-op behavior.

- */ - default void combineVoxelTerrains(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - int shiftX, - int shiftY, - int shiftZ) { - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/legacy/LegacyPhysicsBackendRuntime.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/legacy/LegacyPhysicsBackendRuntime.java deleted file mode 100644 index c147b5dc..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/legacy/LegacyPhysicsBackendRuntime.java +++ /dev/null @@ -1,827 +0,0 @@ -package dev.hytalemodding.impulse.api.runtime.legacy; - -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBackend; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsContact; -import dev.hytalemodding.impulse.api.PhysicsJoint; -import dev.hytalemodding.impulse.api.PhysicsJointType; -import dev.hytalemodding.impulse.api.PhysicsRayHit; -import dev.hytalemodding.impulse.api.PhysicsRuntimeStats; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.PhysicsStepPhaseStats; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.capability.PhysicsActivationTuning; -import dev.hytalemodding.impulse.api.capability.PhysicsActivationTuningCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsCapabilityId; -import dev.hytalemodding.impulse.api.capability.PhysicsContinuousCollisionCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsExtensionSettingsCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsSolverTuning; -import dev.hytalemodding.impulse.api.capability.PhysicsSolverTuningCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsVoxelTerrainCapability; -import dev.hytalemodding.impulse.api.runtime.BackendBodyIdSource; -import dev.hytalemodding.impulse.api.runtime.BackendBodySnapshotSink; -import dev.hytalemodding.impulse.api.runtime.BackendContactSink; -import dev.hytalemodding.impulse.api.runtime.BackendExtensionSettingsSource; -import dev.hytalemodding.impulse.api.runtime.BackendJointType; -import dev.hytalemodding.impulse.api.runtime.BackendRayHitSink; -import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; -import dev.hytalemodding.impulse.api.runtime.BackendRuntimeStatsSink; -import dev.hytalemodding.impulse.api.runtime.BackendStepPhaseStatsSink; -import dev.hytalemodding.impulse.api.runtime.BackendVec3Sink; -import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.Map; -import java.util.Objects; -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -/** - * Id-only runtime facade over the legacy live-object backend API. - */ -@Deprecated(forRemoval = true) -public final class LegacyPhysicsBackendRuntime implements PhysicsBackendRuntime { - - @Nonnull - private final PhysicsBackend backend; - private final Map spaces = new HashMap<>(); - private long nextBodyId = 1L; - private long nextJointId = 1L; - - public LegacyPhysicsBackendRuntime(@Nonnull PhysicsBackend backend) { - this.backend = Objects.requireNonNull(backend, "backend"); - } - - @Override - public int createSpace(@Nonnull SpaceId requestedId) { - Objects.requireNonNull(requestedId, "requestedId"); - if (spaces.containsKey(requestedId.value())) { - throw new IllegalArgumentException("Physics space id=" + requestedId + " is already registered"); - } - PhysicsSpace space = backend.createSpace(requestedId); - if (!requestedId.equals(space.id())) { - try { - space.close(); - } catch (RuntimeException ignored) { - } - throw new IllegalStateException("Backend " + backend.getId() - + " created space id " + space.id() + " but expected " + requestedId); - } - spaces.put(requestedId.value(), new SpaceState(space)); - return requestedId.value(); - } - - @Override - public void destroySpace(int spaceId) { - SpaceState state = spaces.remove(spaceId); - if (state != null) { - state.space.close(); - } - } - - @Override - public void step(int spaceId, float dt) { - requireSpace(spaceId).space.step(dt); - } - - @Override - public void setGravity(int spaceId, float x, float y, float z) { - requireSpace(spaceId).space.setGravity(x, y, z); - } - - @Override - public void getGravity(int spaceId, @Nonnull BackendVec3Sink sink) { - Vector3f gravity = requireSpace(spaceId).space.getGravity(); - sink.accept(gravity.x, gravity.y, gravity.z); - } - - @Override - public long createBody(int spaceId, - int shapeTypeCode, - float halfExtentX, - float halfExtentY, - float halfExtentZ, - float radius, - float halfHeight, - int axisCode, - float groundY, - float mass, - int bodyTypeCode, - float positionX, - float positionY, - float positionZ, - float rotationX, - float rotationY, - float rotationZ, - float rotationW) { - SpaceState state = requireSpace(spaceId); - PhysicsBody body = createLiveBody(state.space, - shapeTypeCode, - halfExtentX, - halfExtentY, - halfExtentZ, - radius, - halfHeight, - axisCode, - groundY, - mass); - body.setBodyType(BackendRuntimeCodes.bodyType(bodyTypeCode)); - body.setPosition(positionX, positionY, positionZ); - body.setRotation(rotationX, rotationY, rotationZ, rotationW); - long bodyId = nextBodyId++; - state.space.addBody(body); - state.bodiesById.put(bodyId, body); - state.bodyIdsByBody.put(body, bodyId); - return bodyId; - } - - @Override - public boolean supportsVoxelTerrain(int spaceId) { - return requireSpace(spaceId).space.getCapability(PhysicsVoxelTerrainCapability.class).isPresent(); - } - - @Override - public long createVoxelTerrain(int spaceId, - float voxelSizeX, - float voxelSizeY, - float voxelSizeZ, - @Nonnull int[] voxelCoordinates, - float positionX, - float positionY, - float positionZ, - float friction, - float restitution, - int collisionGroup, - int collisionMask) { - Objects.requireNonNull(voxelCoordinates, "voxelCoordinates"); - SpaceState state = requireSpace(spaceId); - PhysicsVoxelTerrainCapability capability = requireVoxelTerrainCapability(state); - PhysicsBody body = capability.createVoxelTerrain(voxelSizeX, - voxelSizeY, - voxelSizeZ, - voxelCoordinates); - body.setBodyType(PhysicsBodyType.STATIC); - body.setPosition(positionX, positionY, positionZ); - body.setFriction(friction); - body.setRestitution(restitution); - body.setCollisionFilter(collisionGroup, collisionMask); - - long bodyId = nextBodyId++; - state.space.addBody(body); - state.bodiesById.put(bodyId, body); - state.bodyIdsByBody.put(body, bodyId); - return bodyId; - } - - @Override - public void combineVoxelTerrains(int spaceId, - long bodyAId, - long bodyBId, - int shiftX, - int shiftY, - int shiftZ) { - SpaceState state = requireSpace(spaceId); - PhysicsVoxelTerrainCapability capability = requireVoxelTerrainCapability(state); - capability.combineVoxelTerrains(requireBody(spaceId, bodyAId), - requireBody(spaceId, bodyBId), - shiftX, - shiftY, - shiftZ); - } - - @Override - public void removeBody(int spaceId, long bodyId) { - SpaceState state = requireSpace(spaceId); - PhysicsBody body = state.bodiesById.remove(bodyId); - if (body == null) { - return; - } - state.bodyIdsByBody.remove(body); - state.space.removeBody(body); - } - - @Override - public int bodyCount(int spaceId) { - return requireSpace(spaceId).space.bodyCount(); - } - - @Override - public boolean containsBody(int spaceId, long bodyId) { - SpaceState state = requireSpace(spaceId); - PhysicsBody body = state.bodiesById.get(bodyId); - return body != null && state.space.getBodies().contains(body); - } - - @Override - public boolean bodySnapshot(int spaceId, - long bodyId, - @Nonnull BackendBodySnapshotSink sink) { - SpaceState state = requireSpace(spaceId); - PhysicsBody body = state.bodiesById.get(bodyId); - if (body == null || !state.space.getBodies().contains(body)) { - return false; - } - emitBodySnapshot(bodyId, PhysicsBodySnapshot.from(body), sink); - return true; - } - - @Override - public void snapshotBodies(int spaceId, - @Nonnull BackendBodyIdSource bodyIds, - @Nonnull BackendBodySnapshotSink sink) { - SpaceState state = requireSpace(spaceId); - bodyIds.forEachBodyId(bodyId -> { - PhysicsBody body = state.bodiesById.get(bodyId); - if (body != null && state.space.getBodies().contains(body)) { - emitBodySnapshot(bodyId, PhysicsBodySnapshot.from(body), sink); - } - }); - } - - @Override - public void setBodyTransform(int spaceId, - long bodyId, - float positionX, - float positionY, - float positionZ, - float rotationX, - float rotationY, - float rotationZ, - float rotationW) { - PhysicsBody body = requireBody(spaceId, bodyId); - body.setPosition(positionX, positionY, positionZ); - body.setRotation(rotationX, rotationY, rotationZ, rotationW); - } - - @Override - public void setBodyPosition(int spaceId, long bodyId, float x, float y, float z) { - requireBody(spaceId, bodyId).setPosition(x, y, z); - } - - @Override - public void setBodyVelocity(int spaceId, - long bodyId, - float linearX, - float linearY, - float linearZ, - float angularX, - float angularY, - float angularZ) { - PhysicsBody body = requireBody(spaceId, bodyId); - body.setLinearVelocity(linearX, linearY, linearZ); - body.setAngularVelocity(angularX, angularY, angularZ); - } - - @Override - public void setBodyType(int spaceId, long bodyId, int bodyTypeCode) { - requireBody(spaceId, bodyId).setBodyType(BackendRuntimeCodes.bodyType(bodyTypeCode)); - } - - @Override - public void setBodyDamping(int spaceId, long bodyId, float linearDamping, float angularDamping) { - requireBody(spaceId, bodyId).setDamping(linearDamping, angularDamping); - } - - @Override - public void setBodyFriction(int spaceId, long bodyId, float friction) { - requireBody(spaceId, bodyId).setFriction(friction); - } - - @Override - public void setBodyRestitution(int spaceId, long bodyId, float restitution) { - requireBody(spaceId, bodyId).setRestitution(restitution); - } - - @Override - public void setBodyCollisionFilter(int spaceId, long bodyId, int group, int mask) { - requireBody(spaceId, bodyId).setCollisionFilter(group, mask); - } - - @Override - public void setBodySensor(int spaceId, long bodyId, boolean sensor) { - requireBody(spaceId, bodyId).setSensor(sensor); - } - - @Override - public void setBodyContinuousCollision(int spaceId, long bodyId, boolean enabled) { - requireBody(spaceId, bodyId).setContinuousCollisionEnabled(enabled); - } - - @Override - public boolean isBodyContinuousCollisionEnabled(int spaceId, long bodyId) { - return requireBody(spaceId, bodyId).isContinuousCollisionEnabled(); - } - - @Override - public void activateBody(int spaceId, long bodyId) { - requireBody(spaceId, bodyId).activate(); - } - - @Override - public void sleepBody(int spaceId, long bodyId) { - requireBody(spaceId, bodyId).sleep(); - } - - @Override - public void applyBodyImpulse(int spaceId, - long bodyId, - float x, - float y, - float z, - boolean hasOffset, - float offsetX, - float offsetY, - float offsetZ, - boolean torque) { - PhysicsBody body = requireBody(spaceId, bodyId); - if (torque) { - body.applyTorqueImpulse(x, y, z); - } else if (hasOffset) { - body.applyImpulse(x, y, z, offsetX, offsetY, offsetZ); - } else { - body.applyCentralImpulse(x, y, z); - } - } - - @Override - public void applyBodyForce(int spaceId, - long bodyId, - float x, - float y, - float z, - boolean hasOffset, - float offsetX, - float offsetY, - float offsetZ, - boolean torque) { - PhysicsBody body = requireBody(spaceId, bodyId); - if (torque) { - body.applyTorque(x, y, z); - } else if (hasOffset) { - body.applyForce(x, y, z, offsetX, offsetY, offsetZ); - } else { - body.applyCentralForce(x, y, z); - } - } - - @Override - public long createJoint(int spaceId, - int jointTypeCode, - long bodyAId, - long bodyBId, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ, - float restLength, - float stiffness, - float damping, - float lowerLimit, - float upperLimit, - boolean motorEnabled, - float motorTargetVelocity, - float motorMaxForce) { - SpaceState state = requireSpace(spaceId); - PhysicsBody bodyA = requireBody(spaceId, bodyAId); - PhysicsBody bodyB = requireBody(spaceId, bodyBId); - PhysicsJoint joint = createLiveJoint(state.space, - bodyA, - bodyB, - jointTypeCode, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - axisX, - axisY, - axisZ, - restLength, - stiffness, - damping, - lowerLimit, - upperLimit, - motorEnabled, - motorTargetVelocity, - motorMaxForce); - long jointId = nextJointId++; - state.jointsById.put(jointId, joint); - return jointId; - } - - @Override - public void removeJoint(int spaceId, long jointId) { - SpaceState state = requireSpace(spaceId); - PhysicsJoint joint = state.jointsById.remove(jointId); - if (joint == null) { - return; - } - state.space.removeJoint(joint); - } - - @Override - public int jointCount(int spaceId) { - return requireSpace(spaceId).space.jointCount(); - } - - @Override - public int jointType(int spaceId, long jointId) { - return BackendRuntimeCodes.jointTypeCode(toBackendJointType(requireJoint(spaceId, jointId).getType())); - } - - @Override - public long jointBodyA(int spaceId, long jointId) { - SpaceState state = requireSpace(spaceId); - PhysicsBody body = requireJoint(state, jointId).getBodyA(); - Long bodyId = state.bodyIdsByBody.get(body); - return bodyId != null ? bodyId : -1L; - } - - @Override - public long jointBodyB(int spaceId, long jointId) { - SpaceState state = requireSpace(spaceId); - PhysicsBody body = requireJoint(state, jointId).getBodyB(); - Long bodyId = state.bodyIdsByBody.get(body); - return bodyId != null ? bodyId : -1L; - } - - @Override - public boolean raycastClosest(int spaceId, - float fromX, - float fromY, - float fromZ, - float toX, - float toY, - float toZ, - @Nonnull BackendRayHitSink sink) { - SpaceState state = requireSpace(spaceId); - return state.space.raycastClosest(new Vector3f(fromX, fromY, fromZ), new Vector3f(toX, toY, toZ)) - .map(hit -> emitRayHit(state, hit, sink)) - .orElse(false); - } - - @Override - public int raycastAll(int spaceId, - float fromX, - float fromY, - float fromZ, - float toX, - float toY, - float toZ, - @Nonnull BackendRayHitSink sink) { - SpaceState state = requireSpace(spaceId); - int hits = 0; - for (PhysicsRayHit hit : state.space.raycastAll(new Vector3f(fromX, fromY, fromZ), - new Vector3f(toX, toY, toZ))) { - if (emitRayHit(state, hit, sink)) { - hits++; - } - } - return hits; - } - - @Override - public int contacts(int spaceId, @Nonnull BackendContactSink sink) { - SpaceState state = requireSpace(spaceId); - int contacts = 0; - for (PhysicsContact contact : state.space.getContacts()) { - Long bodyAId = state.bodyIdsByBody.get(contact.bodyA()); - Long bodyBId = state.bodyIdsByBody.get(contact.bodyB()); - if (bodyAId != null && bodyBId != null) { - Vector3f pointOnA = contact.pointOnA(); - Vector3f pointOnB = contact.pointOnB(); - Vector3f normalOnB = contact.normalOnB(); - sink.accept(bodyAId, - bodyBId, - pointOnA.x, - pointOnA.y, - pointOnA.z, - pointOnB.x, - pointOnB.y, - pointOnB.z, - normalOnB.x, - normalOnB.y, - normalOnB.z, - contact.distance(), - contact.impulse()); - contacts++; - } - } - return contacts; - } - - @Override - public int contactCount(int spaceId) { - return requireSpace(spaceId).space.contactCount(); - } - - @Override - public void runtimeStats(int spaceId, @Nonnull BackendRuntimeStatsSink sink) { - PhysicsRuntimeStats stats = requireSpace(spaceId).space.getRuntimeStats(); - sink.accept(stats.bodyCount(), - stats.colliderCount(), - stats.activeBodyCount(), - stats.contactPairCount(), - stats.contactManifoldCount(), - stats.contactPointCount(), - stats.dynamicDynamicContactPairCount(), - stats.terrainContactPairCount(), - stats.activeIslandCount(), - stats.jointCount(), - stats.available()); - } - - @Override - public void resetStepPhaseStats(int spaceId) { - requireSpace(spaceId).space.resetStepPhaseStats(); - } - - @Override - public void stepPhaseStats(int spaceId, @Nonnull BackendStepPhaseStatsSink sink) { - PhysicsStepPhaseStats stats = requireSpace(spaceId).space.getStepPhaseStats(); - sink.accept(stats.stepNanos(), - stats.broadPhaseNanos(), - stats.narrowPhaseNanos(), - stats.solverNanos(), - stats.ccdNanos(), - stats.snapshotNanos(), - stats.available()); - } - - @Override - public boolean supportsContinuousCollision(int spaceId) { - return requireSpace(spaceId).space.getCapability(PhysicsContinuousCollisionCapability.class).isPresent(); - } - - @Override - public boolean supportsSolverTuning(int spaceId) { - return requireSpace(spaceId).space.getCapability(PhysicsSolverTuningCapability.class).isPresent(); - } - - @Override - public boolean supportsActivationTuning(int spaceId) { - return requireSpace(spaceId).space.getCapability(PhysicsActivationTuningCapability.class).isPresent(); - } - - @Override - public void applySolverTuning(int spaceId, @Nonnull PhysicsSolverTuning tuning) { - requireSpace(spaceId).space.getCapability(PhysicsSolverTuningCapability.class) - .ifPresent(capability -> capability.setSolverTuning(tuning)); - } - - @Override - public void applyActivationTuning(int spaceId, @Nonnull PhysicsActivationTuning tuning) { - requireSpace(spaceId).space.getCapability(PhysicsActivationTuningCapability.class) - .ifPresent(capability -> capability.setActivationTuning(tuning)); - } - - @Override - public void applyExtensionSettings(int spaceId, - @Nonnull PhysicsCapabilityId capabilityId, - @Nonnull BackendExtensionSettingsSource settings) { - requireSpace(spaceId).space.getCapability(PhysicsExtensionSettingsCapability.class) - .ifPresent(capability -> { - Map copied = new HashMap<>(); - settings.forEachSetting(copied::put); - capability.applyExtensionSettings(capabilityId, copied); - }); - } - - @Nonnull - private SpaceState requireSpace(int spaceId) { - SpaceState state = spaces.get(spaceId); - if (state == null) { - throw new IllegalArgumentException("Physics space id=" + spaceId + " is not registered"); - } - return state; - } - - @Nonnull - private PhysicsBody requireBody(int spaceId, long bodyId) { - PhysicsBody body = requireSpace(spaceId).bodiesById.get(bodyId); - if (body == null) { - throw new IllegalArgumentException("Physics body id=" + bodyId + " is not registered in space " + spaceId); - } - return body; - } - - @Nonnull - private static PhysicsVoxelTerrainCapability requireVoxelTerrainCapability(@Nonnull SpaceState state) { - return state.space.getCapability(PhysicsVoxelTerrainCapability.class) - .orElseThrow(() -> new UnsupportedOperationException("Legacy backend runtime does not support voxel terrain")); - } - - @Nonnull - private PhysicsJoint requireJoint(int spaceId, long jointId) { - return requireJoint(requireSpace(spaceId), jointId); - } - - @Nonnull - private static PhysicsJoint requireJoint(@Nonnull SpaceState state, long jointId) { - PhysicsJoint joint = state.jointsById.get(jointId); - if (joint == null) { - throw new IllegalArgumentException("Physics joint id=" + jointId + " is not registered"); - } - return joint; - } - - @Nonnull - private static PhysicsBody createLiveBody(@Nonnull PhysicsSpace space, - int shapeTypeCode, - float halfExtentX, - float halfExtentY, - float halfExtentZ, - float radius, - float halfHeight, - int axisCode, - float groundY, - float mass) { - ShapeType shapeType = BackendRuntimeCodes.shapeType(shapeTypeCode); - PhysicsAxis axis = BackendRuntimeCodes.axis(axisCode); - return switch (shapeType) { - case BOX -> space.createBox(halfExtentX, halfExtentY, halfExtentZ, mass); - case SPHERE -> space.createSphere(radius, mass); - case CAPSULE -> space.createCapsule(radius, halfHeight, axis, mass); - case CYLINDER -> space.createCylinder(radius, halfHeight, axis, mass); - case CONE -> space.createCone(radius, halfHeight, axis, mass); - case PLANE -> space.createStaticPlane(groundY); - default -> throw new IllegalArgumentException("Unsupported shape " + shapeType); - }; - } - - @Nonnull - private static PhysicsJoint createLiveJoint(@Nonnull PhysicsSpace space, - @Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - int jointTypeCode, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ, - float restLength, - float stiffness, - float damping, - float lowerLimit, - float upperLimit, - boolean motorEnabled, - float motorTargetVelocity, - float motorMaxForce) { - BackendJointType type = BackendRuntimeCodes.jointType(jointTypeCode); - PhysicsJoint joint = switch (type) { - case FIXED -> space.createFixedJoint(bodyA, - bodyB, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ); - case POINT -> space.createPointJoint(bodyA, - bodyB, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ); - case HINGE -> space.createHingeJoint(bodyA, - bodyB, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - axisX, - axisY, - axisZ); - case SLIDER -> space.createSliderJoint(bodyA, - bodyB, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - axisX, - axisY, - axisZ); - case SPRING -> space.createSpringJoint(bodyA, - bodyB, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - restLength, - stiffness, - damping); - }; - if (type == BackendJointType.HINGE || type == BackendJointType.SLIDER) { - joint.setLimits(lowerLimit, upperLimit); - joint.setMotor(motorTargetVelocity, motorMaxForce); - joint.setMotorEnabled(motorEnabled); - } - return joint; - } - - private static boolean emitRayHit(@Nonnull SpaceState state, - @Nonnull PhysicsRayHit hit, - @Nonnull BackendRayHitSink sink) { - Long bodyId = state.bodyIdsByBody.get(hit.body()); - if (bodyId == null) { - return false; - } - Vector3f point = hit.point(); - Vector3f normal = hit.normal(); - sink.accept(bodyId, - point.x, - point.y, - point.z, - normal.x, - normal.y, - normal.z, - hit.fraction(), - hit.distance()); - return true; - } - - private static void emitBodySnapshot(long bodyId, - @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull BackendBodySnapshotSink sink) { - sink.accept(bodyId, - BackendRuntimeCodes.shapeTypeCode(snapshot.shapeType()), - BackendRuntimeCodes.bodyTypeCode(snapshot.bodyType()), - snapshot.positionX(), - snapshot.positionY(), - snapshot.positionZ(), - snapshot.rotationX(), - snapshot.rotationY(), - snapshot.rotationZ(), - snapshot.rotationW(), - snapshot.linearVelocityX(), - snapshot.linearVelocityY(), - snapshot.linearVelocityZ(), - snapshot.angularVelocityX(), - snapshot.angularVelocityY(), - snapshot.angularVelocityZ(), - snapshot.sleeping(), - snapshot.sensor(), - snapshot.mass(), - snapshot.friction(), - snapshot.restitution(), - snapshot.linearDamping(), - snapshot.angularDamping(), - snapshot.collisionGroup(), - snapshot.collisionMask(), - snapshot.continuousCollisionEnabled(), - snapshot.centerOfMassOffsetY(), - snapshot.hasBoxHalfExtents(), - snapshot.boxHalfExtentX(), - snapshot.boxHalfExtentY(), - snapshot.boxHalfExtentZ(), - snapshot.sphereRadius(), - snapshot.halfHeight(), - BackendRuntimeCodes.axisCode(snapshot.shapeAxis())); - } - - @Nonnull - private static BackendJointType toBackendJointType(@Nonnull PhysicsJointType type) { - return switch (type) { - case FIXED -> BackendJointType.FIXED; - case POINT -> BackendJointType.POINT; - case HINGE -> BackendJointType.HINGE; - case SLIDER -> BackendJointType.SLIDER; - case SPRING -> BackendJointType.SPRING; - }; - } - - private static final class SpaceState { - - @Nonnull - private final PhysicsSpace space; - private final Map bodiesById = new HashMap<>(); - private final Map bodyIdsByBody = new IdentityHashMap<>(); - private final Map jointsById = new HashMap<>(); - - private SpaceState(@Nonnull PhysicsSpace space) { - this.space = Objects.requireNonNull(space, "space"); - } - } -} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/legacy/LegacyPhysicsBackendRuntimeProvider.java b/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/legacy/LegacyPhysicsBackendRuntimeProvider.java deleted file mode 100644 index 661267be..00000000 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/legacy/LegacyPhysicsBackendRuntimeProvider.java +++ /dev/null @@ -1,45 +0,0 @@ -package dev.hytalemodding.impulse.api.runtime.legacy; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.PhysicsBackend; -import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; -import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; -import java.util.Objects; -import java.util.logging.Level; -import javax.annotation.Nonnull; - -/** - * Runtime-provider adapter for legacy object-based backends. - */ -@Deprecated(forRemoval = true) -public final class LegacyPhysicsBackendRuntimeProvider implements PhysicsBackendRuntimeProvider { - - @Nonnull - private final PhysicsBackend backend; - - public LegacyPhysicsBackendRuntimeProvider(@Nonnull PhysicsBackend backend) { - this.backend = Objects.requireNonNull(backend, "backend"); - } - - @Nonnull - @Override - public BackendId getId() { - return backend.getId(); - } - - @Override - public void init() { - backend.init(); - } - - @Override - public void setInternalLoggingLevel(@Nonnull Level level) { - backend.setInternalLoggingLevel(level); - } - - @Nonnull - @Override - public PhysicsBackendRuntime createRuntime() { - return new LegacyPhysicsBackendRuntime(backend); - } -} diff --git a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/PhysicsBackendEventBufferTest.java b/impulse-api/src/test/java/dev/hytalemodding/impulse/api/PhysicsBackendEventBufferTest.java deleted file mode 100644 index 277f80e3..00000000 --- a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/PhysicsBackendEventBufferTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package dev.hytalemodding.impulse.api; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class PhysicsBackendEventBufferTest { - - @Test - void buffersCopiedContactEventsUntilCapacityAndReportsDrops() { - FakePhysicsBackend backend = new FakePhysicsBackend("test:event-buffer"); - PhysicsSpace space = backend.createSpace(); - PhysicsBody first = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody second = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - Vector3f pointOnA = new Vector3f(1.0f, 2.0f, 3.0f); - Vector3f pointOnB = new Vector3f(4.0f, 5.0f, 6.0f); - Vector3f normalOnB = new Vector3f(0.0f, 1.0f, 0.0f); - PhysicsBackendEventBuffer buffer = new PhysicsBackendEventBuffer(1); - - assertTrue(buffer.contact(PhysicsContactPhase.OBSERVED, - first, - second, - pointOnA, - pointOnB, - normalOnB, - -0.125f, - 2.5f)); - pointOnA.set(7.0f, 8.0f, 9.0f); - assertFalse(buffer.contact(PhysicsContactPhase.OBSERVED, - first, - second, - pointOnA, - pointOnB, - normalOnB, - -0.25f, - 3.5f)); - - PhysicsBackendEventBatch batch = buffer.drain(); - - assertEquals(1, batch.size()); - assertEquals(1, batch.droppedEventCount()); - PhysicsBackendContactEvent event = (PhysicsBackendContactEvent) batch.events().getFirst(); - assertEquals(PhysicsBackendEventKind.CONTACT_OBSERVED, event.kind()); - assertEquals(PhysicsContactPhase.OBSERVED, event.phase()); - assertEquals(first, event.bodyA()); - assertEquals(second, event.bodyB()); - assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), event.pointOnA()); - assertEquals(new Vector3f(4.0f, 5.0f, 6.0f), event.pointOnB()); - assertEquals(new Vector3f(0.0f, 1.0f, 0.0f), event.normalOnB()); - assertEquals(-0.125f, event.distance()); - assertEquals(2.5f, event.impulse()); - assertTrue(buffer.drain().isEmpty()); - } -} diff --git a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/capability/PhysicsBackendEventsCapabilityTest.java b/impulse-api/src/test/java/dev/hytalemodding/impulse/api/capability/PhysicsBackendEventsCapabilityTest.java deleted file mode 100644 index c3808082..00000000 --- a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/capability/PhysicsBackendEventsCapabilityTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package dev.hytalemodding.impulse.api.capability; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.PhysicsBackendEventKind; -import dev.hytalemodding.impulse.api.PhysicsContactPhase; -import java.util.Set; -import org.junit.jupiter.api.Test; - -class PhysicsBackendEventsCapabilityTest { - - @Test - void mapsSupportedContactPhasesThroughBackendEventKinds() { - PhysicsBackendEventsCapability capability = - () -> Set.of(PhysicsBackendEventKind.CONTACT_STARTED, PhysicsBackendEventKind.CONTACT_ENDED); - - assertTrue(capability.supportsContactPhase(PhysicsContactPhase.STARTED)); - assertTrue(capability.supportsContactPhase(PhysicsContactPhase.ENDED)); - assertFalse(capability.supportsContactPhase(PhysicsContactPhase.FORCE)); - } -} diff --git a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/runtime/legacy/LegacyPhysicsBackendRuntimeTest.java b/impulse-api/src/test/java/dev/hytalemodding/impulse/api/runtime/legacy/LegacyPhysicsBackendRuntimeTest.java deleted file mode 100644 index 12a78352..00000000 --- a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/runtime/legacy/LegacyPhysicsBackendRuntimeTest.java +++ /dev/null @@ -1,415 +0,0 @@ -package dev.hytalemodding.impulse.api.runtime.legacy; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.PhysicsBackend; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.capability.PhysicsVoxelTerrainCapability; -import dev.hytalemodding.impulse.api.runtime.BackendBodySnapshotSink; -import dev.hytalemodding.impulse.api.runtime.BackendJointType; -import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import javax.annotation.Nonnull; -import org.junit.jupiter.api.Test; - -class LegacyPhysicsBackendRuntimeTest { - - @Test - void wrapsLegacyBackendWithNumericBodyAndJointIds() { - LegacyPhysicsBackendRuntime runtime = - new LegacyPhysicsBackendRuntime(new FakePhysicsBackend("impulse:test")); - - int spaceId = runtime.createSpace(new SpaceId(9001)); - long bodyA = runtime.createBody(spaceId, - BackendRuntimeCodes.SHAPE_SPHERE, - 0.0f, - 0.0f, - 0.0f, - 0.5f, - 0.0f, - BackendRuntimeCodes.AXIS_Y, - 0.0f, - 1.0f, - BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC), - 0.0f, - 3.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 1.0f); - long bodyB = runtime.createBody(spaceId, - BackendRuntimeCodes.SHAPE_BOX, - 0.5f, - 0.5f, - 0.5f, - 0.0f, - 0.0f, - BackendRuntimeCodes.AXIS_Y, - 0.0f, - 1.0f, - BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC), - 1.0f, - 3.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 1.0f); - long joint = runtime.createJoint(spaceId, - BackendRuntimeCodes.JOINT_POINT, - bodyA, - bodyB, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 1.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - false, - 0.0f, - 0.0f); - - assertEquals(9001, spaceId); - assertNotEquals(bodyA, bodyB); - assertTrue(bodyA > 0L); - assertTrue(bodyB > 0L); - assertTrue(joint > 0L); - assertEquals(2, runtime.bodyCount(spaceId)); - assertEquals(1, runtime.jointCount(spaceId)); - - runtime.setBodyFriction(spaceId, bodyA, 0.65f); - runtime.setBodyRestitution(spaceId, bodyA, 0.15f); - runtime.setBodyDamping(spaceId, bodyA, 0.2f, 0.3f); - runtime.setBodyCollisionFilter(spaceId, bodyA, 2, 3); - runtime.setBodyContinuousCollision(spaceId, bodyA, true); - - CapturedBodySnapshot snapshot = new CapturedBodySnapshot(); - boolean snapshotPresent = runtime.bodySnapshot(spaceId, - bodyA, - snapshot); - - assertTrue(snapshotPresent); - assertEquals(ShapeType.SPHERE, BackendRuntimeCodes.shapeType(snapshot.shapeTypeCode)); - assertEquals(BackendRuntimeCodes.AXIS_Y, snapshot.axisCode); - assertEquals(1.0f, snapshot.mass, 0.0001f); - assertEquals(0.65f, snapshot.friction, 0.0001f); - assertEquals(0.15f, snapshot.restitution, 0.0001f); - assertEquals(0.2f, snapshot.linearDamping, 0.0001f); - assertEquals(0.3f, snapshot.angularDamping, 0.0001f); - assertEquals(2, snapshot.collisionGroup); - assertEquals(3, snapshot.collisionMask); - assertTrue(snapshot.continuousCollisionEnabled); - assertEquals(BackendRuntimeCodes.jointTypeCode(BackendJointType.POINT), runtime.jointType(spaceId, joint)); - assertEquals(bodyA, runtime.jointBodyA(spaceId, joint)); - assertEquals(bodyB, runtime.jointBodyB(spaceId, joint)); - } - - @Test - void createSpaceRejectsLegacyBackendThatReturnsDifferentExplicitId() { - LegacyPhysicsBackendRuntime runtime = - new LegacyPhysicsBackendRuntime(new MismatchedExplicitSpaceBackend("impulse:test-mismatch")); - - IllegalStateException failure = assertThrows(IllegalStateException.class, - () -> runtime.createSpace(new SpaceId(9004))); - - assertTrue(failure.getMessage().contains("created space id")); - assertTrue(failure.getMessage().contains("9004")); - } - - @Test - void legacyRuntimeRejectsVoxelTerrainWhenCapabilityMissing() { - LegacyPhysicsBackendRuntime runtime = - new LegacyPhysicsBackendRuntime(new FakePhysicsBackend("impulse:test-voxel")); - int spaceId = runtime.createSpace(new SpaceId(9002)); - - assertFalse(runtime.supportsVoxelTerrain(spaceId)); - assertThrows(UnsupportedOperationException.class, - () -> runtime.createVoxelTerrain(spaceId, - 1.0f, - 1.0f, - 1.0f, - new int[] { 0, 0, 0 }, - 0.0f, - 0.0f, - 0.0f, - 0.75f, - 0.0f, - 1, - -1)); - assertThrows(UnsupportedOperationException.class, - () -> runtime.combineVoxelTerrains(spaceId, 1L, 2L, 16, 0, 0)); - } - - @Test - void legacyRuntimePassesThroughVoxelTerrainCapability() { - RecordingVoxelBackend backend = new RecordingVoxelBackend("impulse:test-voxel-passthrough"); - LegacyPhysicsBackendRuntime runtime = new LegacyPhysicsBackendRuntime(backend); - int spaceId = runtime.createSpace(new SpaceId(9003)); - - assertTrue(runtime.supportsVoxelTerrain(spaceId)); - long first = runtime.createVoxelTerrain(spaceId, - 1.0f, - 1.0f, - 1.0f, - new int[] { 0, 0, 0 }, - 8.0f, - 16.0f, - 24.0f, - 0.75f, - 0.0f, - 2, - 4); - long second = runtime.createVoxelTerrain(spaceId, - 1.0f, - 1.0f, - 1.0f, - new int[] { 1, 0, 0 }, - 24.0f, - 16.0f, - 24.0f, - 0.5f, - 0.1f, - 8, - 16); - - assertEquals(2, runtime.bodyCount(spaceId)); - assertTrue(runtime.containsBody(spaceId, first)); - PhysicsBody firstBody = backend.voxelBodies().getFirst(); - assertEquals(8.0f, firstBody.getPosition().x); - assertEquals(16.0f, firstBody.getPosition().y); - assertEquals(24.0f, firstBody.getPosition().z); - assertEquals(0.75f, firstBody.getFriction()); - assertEquals(0.0f, firstBody.getRestitution()); - assertEquals(2, firstBody.getCollisionGroup()); - assertEquals(4, firstBody.getCollisionMask()); - - runtime.combineVoxelTerrains(spaceId, first, second, 16, 0, 0); - - assertEquals(List.of(new CombineCall(firstBody, backend.voxelBodies().get(1), 16, 0, 0)), - backend.combineCalls()); - } - - private record CombineCall(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - int shiftX, - int shiftY, - int shiftZ) { - } - - private static final class CapturedBodySnapshot implements BackendBodySnapshotSink { - - private int shapeTypeCode = BackendRuntimeCodes.SHAPE_UNKNOWN; - private int axisCode = -1; - private float mass; - private float friction; - private float restitution; - private float linearDamping; - private float angularDamping; - private int collisionGroup; - private int collisionMask; - private boolean continuousCollisionEnabled; - - @Override - public void accept(long bodyId, - int shapeTypeCode, - int bodyTypeCode, - float positionX, - float positionY, - float positionZ, - float rotationX, - float rotationY, - float rotationZ, - float rotationW, - float linearVelocityX, - float linearVelocityY, - float linearVelocityZ, - float angularVelocityX, - float angularVelocityY, - float angularVelocityZ, - boolean sleeping, - boolean sensor, - float mass, - float friction, - float restitution, - float linearDamping, - float angularDamping, - int collisionGroup, - int collisionMask, - boolean continuousCollisionEnabled, - float centerOfMassOffsetY, - boolean hasBoxHalfExtents, - float halfExtentX, - float halfExtentY, - float halfExtentZ, - float radius, - float halfHeight, - int axisCode) { - this.shapeTypeCode = shapeTypeCode; - this.axisCode = axisCode; - this.mass = mass; - this.friction = friction; - this.restitution = restitution; - this.linearDamping = linearDamping; - this.angularDamping = angularDamping; - this.collisionGroup = collisionGroup; - this.collisionMask = collisionMask; - this.continuousCollisionEnabled = continuousCollisionEnabled; - } - } - - private static final class MismatchedExplicitSpaceBackend implements PhysicsBackend { - - @Nonnull - private final BackendId id; - @Nonnull - private final FakePhysicsBackend delegate; - - private MismatchedExplicitSpaceBackend(@Nonnull String id) { - this.id = new BackendId(id); - this.delegate = new FakePhysicsBackend(this.id); - } - - @Nonnull - @Override - public BackendId getId() { - return id; - } - - @Override - public void init() { - } - - @Nonnull - @Override - public PhysicsSpace createSpace() { - return delegate.createSpace(); - } - - @Nonnull - @Override - public PhysicsSpace createSpace(@Nonnull SpaceId spaceId) { - return delegate.createSpace(new SpaceId(spaceId.value() + 1)); - } - } - - private static final class RecordingVoxelBackend implements PhysicsBackend { - - @Nonnull - private final BackendId id; - @Nonnull - private final FakePhysicsBackend delegate; - @Nonnull - private final RecordingVoxelCapability voxelCapability = new RecordingVoxelCapability(); - - private RecordingVoxelBackend(@Nonnull String id) { - this.id = new BackendId(id); - this.delegate = new FakePhysicsBackend(this.id); - } - - @Nonnull - @Override - public BackendId getId() { - return id; - } - - @Override - public void init() { - } - - @Nonnull - @Override - public PhysicsSpace createSpace() { - return createSpace(SpaceId.next()); - } - - @Nonnull - @Override - public PhysicsSpace createSpace(@Nonnull SpaceId spaceId) { - PhysicsSpace space = delegate.createSpace(spaceId); - return (PhysicsSpace) Proxy.newProxyInstance(PhysicsSpace.class.getClassLoader(), - new Class[] { PhysicsSpace.class }, - new RecordingVoxelSpace(space, voxelCapability)); - } - - @Nonnull - private List voxelBodies() { - return List.copyOf(voxelCapability.voxelBodies); - } - - @Nonnull - private List combineCalls() { - return List.copyOf(voxelCapability.combineCalls); - } - } - - private record RecordingVoxelSpace(@Nonnull PhysicsSpace delegate, - @Nonnull RecordingVoxelCapability voxelCapability) - implements InvocationHandler { - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - if ("getCapability".equals(method.getName()) && args != null && args.length == 1 - && args[0] == PhysicsVoxelTerrainCapability.class) { - return Optional.of(voxelCapability); - } - try { - return method.invoke(delegate, args); - } catch (InvocationTargetException exception) { - throw exception.getCause(); - } - } - } - - private static final class RecordingVoxelCapability implements PhysicsVoxelTerrainCapability { - - private final List voxelBodies = new ArrayList<>(); - private final List combineCalls = new ArrayList<>(); - - @Nonnull - @Override - public PhysicsBody createVoxelTerrain(float voxelSizeX, - float voxelSizeY, - float voxelSizeZ, - @Nonnull int[] voxelCoordinates) { - PhysicsBody body = new FakePhysicsBackend("impulse:test-voxel-body") - .createSpace() - .createBox(0.5f, 0.5f, 0.5f, 0.0f); - voxelBodies.add(body); - return body; - } - - @Override - public void combineVoxelTerrains(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - int shiftX, - int shiftY, - int shiftZ) { - combineCalls.add(new CombineCall(bodyA, bodyB, shiftX, shiftY, shiftZ)); - } - } -} diff --git a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/testsupport/FakePhysicsBackendCapabilityTest.java b/impulse-api/src/test/java/dev/hytalemodding/impulse/api/testsupport/FakePhysicsBackendCapabilityTest.java deleted file mode 100644 index 3952d383..00000000 --- a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/testsupport/FakePhysicsBackendCapabilityTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package dev.hytalemodding.impulse.api.testsupport; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.capability.PhysicsActivationTuning; -import dev.hytalemodding.impulse.api.capability.PhysicsActivationTuningCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsSolverTuning; -import dev.hytalemodding.impulse.api.capability.PhysicsSolverTuningCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsVoxelTerrainCapability; -import org.junit.jupiter.api.Test; - -class FakePhysicsBackendCapabilityTest { - - @Test - void appliesSolverAndActivationSettingsThroughCapabilities() { - FakePhysicsBackend backend = new FakePhysicsBackend("test:fake"); - PhysicsSpace space = backend.createSpace(); - - PhysicsSolverTuningCapability solverTuning = space.getCapability(PhysicsSolverTuningCapability.class) - .orElseThrow(); - PhysicsActivationTuningCapability activationTuning = space.getCapability(PhysicsActivationTuningCapability.class) - .orElseThrow(); - - solverTuning.setSolverTuning(new PhysicsSolverTuning(12, 3)); - activationTuning.setActivationTuning(new PhysicsActivationTuning(0.25f, 0.5f, 1.75f)); - - FakePhysicsBackend.InMemoryPhysicsSpace inMemorySpace = backend.createdSpaces().get(0); - assertEquals(12, inMemorySpace.getSolverIterations()); - assertEquals(3, inMemorySpace.getStabilizationIterations()); - assertEquals(0.25f, inMemorySpace.getSleepLinearThreshold(), 0.0001f); - assertEquals(0.5f, inMemorySpace.getSleepAngularThreshold(), 0.0001f); - assertEquals(1.75f, inMemorySpace.getSleepTimeUntilSleep(), 0.0001f); - } - - @Test - void doesNotExposeVoxelCapability() { - FakePhysicsBackend backend = new FakePhysicsBackend("test:fake"); - PhysicsSpace space = backend.createSpace(); - - assertTrue(space.getCapability(PhysicsVoxelTerrainCapability.class).isEmpty()); - } -} diff --git a/impulse-api/src/testFixtures/java/dev/hytalemodding/impulse/api/testsupport/FakePhysicsBackend.java b/impulse-api/src/testFixtures/java/dev/hytalemodding/impulse/api/testsupport/FakePhysicsBackend.java deleted file mode 100644 index fd774d45..00000000 --- a/impulse-api/src/testFixtures/java/dev/hytalemodding/impulse/api/testsupport/FakePhysicsBackend.java +++ /dev/null @@ -1,805 +0,0 @@ -package dev.hytalemodding.impulse.api.testsupport; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBackend; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsContact; -import dev.hytalemodding.impulse.api.PhysicsBackendEventSink; -import dev.hytalemodding.impulse.api.PhysicsContactPhase; -import dev.hytalemodding.impulse.api.PhysicsJoint; -import dev.hytalemodding.impulse.api.PhysicsJointType; -import dev.hytalemodding.impulse.api.PhysicsRayHit; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.capability.PhysicsActivationTuning; -import dev.hytalemodding.impulse.api.capability.PhysicsActivationTuningCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsCapabilityDescriptor; -import dev.hytalemodding.impulse.api.capability.PhysicsSolverTuning; -import dev.hytalemodding.impulse.api.capability.PhysicsSolverTuningCapability; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -/** - * Small in-memory backend for resource lifecycle tests that need real API objects - * without loading a native physics implementation. - */ -public final class FakePhysicsBackend implements PhysicsBackend { - - private final BackendId id; - private final List createdSpaces = new ArrayList<>(); - - public FakePhysicsBackend(@Nonnull String id) { - this(new BackendId(id)); - } - - public FakePhysicsBackend(@Nonnull BackendId id) { - this.id = id; - } - - @Nonnull - @Override - public BackendId getId() { - return id; - } - - @Override - public void init() { - } - - @Nonnull - @Override - public PhysicsSpace createSpace() { - return createSpace(SpaceId.next()); - } - - @Nonnull - @Override - public PhysicsSpace createSpace(@Nonnull SpaceId spaceId) { - InMemoryPhysicsSpace space = new InMemoryPhysicsSpace(spaceId, id); - createdSpaces.add(space); - return space; - } - - @Nonnull - public List createdSpaces() { - return List.copyOf(createdSpaces); - } - - public static final class InMemoryPhysicsSpace implements PhysicsSpace, - PhysicsSolverTuningCapability, - PhysicsActivationTuningCapability { - - private final SpaceId id; - private final BackendId backendId; - private final List bodies = new ArrayList<>(); - private final List joints = new ArrayList<>(); - private final List contacts = new ArrayList<>(); - private final Vector3f gravity = new Vector3f(); - private boolean closed; - private int solverIterations; - private int stabilizationIterations; - private float sleepLinearThreshold; - private float sleepAngularThreshold; - private float sleepTimeUntilSleep; - - private InMemoryPhysicsSpace(@Nonnull SpaceId id, @Nonnull BackendId backendId) { - this.id = id; - this.backendId = backendId; - } - - @Nonnull - @Override - public SpaceId id() { - return id; - } - - @Nonnull - @Override - public BackendId backendId() { - return backendId; - } - - @Override - public void step(float dt) { - } - - @Override - public void step(float dt, @Nonnull PhysicsBackendEventSink events) { - for (PhysicsContact contact : contacts) { - events.contact(PhysicsContactPhase.OBSERVED, - contact.bodyA(), - contact.bodyB(), - contact.pointOnA(), - contact.pointOnB(), - contact.normalOnB(), - contact.distance(), - contact.impulse()); - } - } - - @Override - public void setGravity(float x, float y, float z) { - gravity.set(x, y, z); - } - - @Nonnull - @Override - public Vector3f getGravity() { - return new Vector3f(gravity); - } - - @Override - public void addBody(@Nonnull PhysicsBody body) { - bodies.add(body); - } - - @Override - public void removeBody(@Nonnull PhysicsBody body) { - bodies.remove(body); - } - - @Nonnull - @Override - public List getBodies() { - return new ArrayList<>(bodies); - } - - @Override - public int bodyCount() { - return bodies.size(); - } - - @Nonnull - @Override - public Optional getCapability(@Nonnull Class type) { - Objects.requireNonNull(type, "type"); - if (type == PhysicsSolverTuningCapability.class || type == PhysicsActivationTuningCapability.class) { - return Optional.of(type.cast(this)); - } - return Optional.empty(); - } - - @Nonnull - @Override - public List getCapabilityDescriptors() { - return List.of(PhysicsSolverTuningCapability.DESCRIPTOR, PhysicsActivationTuningCapability.DESCRIPTOR); - } - - @Nonnull - @Override - public PhysicsBody createStaticPlane(float groundY) { - InMemoryPhysicsBody body = new InMemoryPhysicsBody(ShapeType.PLANE, PhysicsBodyType.STATIC); - body.position.y = groundY; - return body; - } - - @Nonnull - @Override - public PhysicsBody createBox(float halfX, float halfY, float halfZ, float mass) { - InMemoryPhysicsBody body = new InMemoryPhysicsBody(ShapeType.BOX, PhysicsBodyType.DYNAMIC); - body.halfExtents.set(halfX, halfY, halfZ); - body.mass = mass; - return body; - } - - @Nonnull - @Override - public PhysicsBody createBox(@Nonnull Vector3f halfExtents, float mass) { - return createBox(halfExtents.x, halfExtents.y, halfExtents.z, mass); - } - - @Nonnull - @Override - public PhysicsBody createSphere(float radius, float mass) { - InMemoryPhysicsBody body = new InMemoryPhysicsBody(ShapeType.SPHERE, PhysicsBodyType.DYNAMIC); - body.radius = radius; - body.mass = mass; - return body; - } - - @Nonnull - @Override - public PhysicsBody createCapsule(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - return createRoundHeightBody(ShapeType.CAPSULE, radius, halfHeight, axis, mass); - } - - @Nonnull - @Override - public PhysicsBody createCylinder(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - return createRoundHeightBody(ShapeType.CYLINDER, radius, halfHeight, axis, mass); - } - - @Nonnull - @Override - public PhysicsBody createCone(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - return createRoundHeightBody(ShapeType.CONE, radius, halfHeight, axis, mass); - } - - @Nonnull - @Override - public Optional raycastClosest(@Nonnull Vector3f from, @Nonnull Vector3f to) { - return Optional.empty(); - } - - @Nonnull - @Override - public List raycastAll(@Nonnull Vector3f from, @Nonnull Vector3f to) { - return List.of(); - } - - @Nonnull - @Override - public List getContacts() { - return new ArrayList<>(contacts); - } - - public void addContact(@Nonnull PhysicsContact contact) { - contacts.add(contact); - } - - @Nonnull - @Override - public PhysicsJoint createFixedJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB) { - return addJoint(PhysicsJointType.FIXED, bodyA, bodyB, anchorA, anchorB, null); - } - - @Nonnull - @Override - public PhysicsJoint createPointJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB) { - return addJoint(PhysicsJointType.POINT, bodyA, bodyB, anchorA, anchorB, null); - } - - @Nonnull - @Override - public PhysicsJoint createHingeJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis) { - return addJoint(PhysicsJointType.HINGE, bodyA, bodyB, anchorA, anchorB, axis); - } - - @Nonnull - @Override - public PhysicsJoint createSliderJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis) { - return addJoint(PhysicsJointType.SLIDER, bodyA, bodyB, anchorA, anchorB, axis); - } - - @Nonnull - @Override - public PhysicsJoint createSpringJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - float restLength, - float stiffness, - float damping) { - return addJoint(PhysicsJointType.SPRING, bodyA, bodyB, anchorA, anchorB, null); - } - - @Override - public void removeJoint(@Nonnull PhysicsJoint joint) { - joints.remove(joint); - } - - @Nonnull - @Override - public List getJoints() { - return new ArrayList<>(joints); - } - - @Override - public int jointCount() { - return joints.size(); - } - - @Override - public void close() { - closed = true; - } - - @Override - public void setSolverTuning(@Nonnull PhysicsSolverTuning tuning) { - Objects.requireNonNull(tuning, "tuning"); - solverIterations = tuning.solverIterations(); - stabilizationIterations = tuning.stabilizationIterations(); - } - - @Override - public void setActivationTuning(@Nonnull PhysicsActivationTuning tuning) { - Objects.requireNonNull(tuning, "tuning"); - sleepLinearThreshold = tuning.linearSleepThreshold(); - sleepAngularThreshold = tuning.angularSleepThreshold(); - sleepTimeUntilSleep = tuning.timeUntilSleep(); - } - - public boolean isClosed() { - return closed; - } - - public int getSolverIterations() { - return solverIterations; - } - - public int getStabilizationIterations() { - return stabilizationIterations; - } - - public float getSleepLinearThreshold() { - return sleepLinearThreshold; - } - - public float getSleepAngularThreshold() { - return sleepAngularThreshold; - } - - public float getSleepTimeUntilSleep() { - return sleepTimeUntilSleep; - } - - @Nonnull - private PhysicsBody createRoundHeightBody(@Nonnull ShapeType shapeType, - float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - InMemoryPhysicsBody body = new InMemoryPhysicsBody(shapeType, PhysicsBodyType.DYNAMIC); - body.radius = radius; - body.halfHeight = halfHeight; - body.axis = axis; - body.mass = mass; - return body; - } - - @Nonnull - private PhysicsJoint addJoint(@Nonnull PhysicsJointType type, - @Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nullable Vector3f axis) { - PhysicsJoint joint = new InMemoryPhysicsJoint(type, bodyA, bodyB, anchorA, anchorB, axis); - joints.add(joint); - return joint; - } - } - - private static final class InMemoryPhysicsBody implements PhysicsBody { - - private final ShapeType shapeType; - private final Vector3f position = new Vector3f(); - private final Quaternionf rotation = new Quaternionf(); - private final Vector3f linearVelocity = new Vector3f(); - private final Vector3f angularVelocity = new Vector3f(); - private final Vector3f halfExtents = new Vector3f(); - private PhysicsBodyType bodyType; - private PhysicsAxis axis = PhysicsAxis.Y; - private float mass = 1.0f; - private float radius; - private float halfHeight; - private float restitution; - private float friction; - private float linearDamping; - private float angularDamping; - private boolean sensor; - private boolean continuousCollision; - private int collisionGroup; - private int collisionMask; - - private InMemoryPhysicsBody(@Nonnull ShapeType shapeType, @Nonnull PhysicsBodyType bodyType) { - this.shapeType = shapeType; - this.bodyType = bodyType; - } - - @Override - public void setPosition(float x, float y, float z) { - position.set(x, y, z); - } - - @Override - public void setPosition(@Nonnull Vector3f pos) { - position.set(pos); - } - - @Nonnull - @Override - public Vector3f getPosition() { - return new Vector3f(position); - } - - @Override - public void setRotation(float x, float y, float z, float w) { - rotation.set(x, y, z, w); - } - - @Override - public void setRotation(@Nonnull Quaternionf rot) { - rotation.set(rot); - } - - @Nonnull - @Override - public Quaternionf getRotation() { - return new Quaternionf(rotation); - } - - @Override - public void setRestitution(float restitution) { - this.restitution = restitution; - } - - @Override - public float getRestitution() { - return restitution; - } - - @Override - public void setFriction(float friction) { - this.friction = friction; - } - - @Override - public float getFriction() { - return friction; - } - - @Nonnull - @Override - public PhysicsBodyType getBodyType() { - return bodyType; - } - - @Override - public void setBodyType(@Nonnull PhysicsBodyType bodyType) { - this.bodyType = bodyType; - } - - @Override - public boolean isStatic() { - return bodyType == PhysicsBodyType.STATIC; - } - - @Override - public boolean isKinematic() { - return bodyType == PhysicsBodyType.KINEMATIC; - } - - @Override - public void setKinematic(boolean kinematic) { - bodyType = kinematic ? PhysicsBodyType.KINEMATIC : PhysicsBodyType.DYNAMIC; - } - - @Override - public void activate() { - } - - @Override - public boolean isActive() { - return true; - } - - @Override - public boolean isSleeping() { - return false; - } - - @Override - public void sleep() { - } - - @Override - public float getMass() { - return mass; - } - - @Override - public void setMass(float mass) { - this.mass = mass; - } - - @Nonnull - @Override - public Vector3f getLinearVelocity() { - return new Vector3f(linearVelocity); - } - - @Override - public void setLinearVelocity(@Nonnull Vector3f vel) { - linearVelocity.set(vel); - } - - @Override - public void setLinearVelocity(float x, float y, float z) { - linearVelocity.set(x, y, z); - } - - @Nonnull - @Override - public Vector3f getAngularVelocity() { - return new Vector3f(angularVelocity); - } - - @Override - public void setAngularVelocity(@Nonnull Vector3f vel) { - angularVelocity.set(vel); - } - - @Override - public void setAngularVelocity(float x, float y, float z) { - angularVelocity.set(x, y, z); - } - - @Override - public float getLinearDamping() { - return linearDamping; - } - - @Override - public void setLinearDamping(float damping) { - linearDamping = damping; - } - - @Override - public float getAngularDamping() { - return angularDamping; - } - - @Override - public void setAngularDamping(float damping) { - angularDamping = damping; - } - - @Override - public void applyCentralForce(@Nonnull Vector3f force) { - } - - @Override - public void applyCentralForce(float x, float y, float z) { - } - - @Override - public void applyForce(@Nonnull Vector3f force, @Nonnull Vector3f offset) { - } - - @Override - public void applyCentralImpulse(@Nonnull Vector3f impulse) { - } - - @Override - public void applyCentralImpulse(float x, float y, float z) { - } - - @Override - public void applyImpulse(@Nonnull Vector3f impulse, @Nonnull Vector3f offset) { - } - - @Override - public void applyTorque(@Nonnull Vector3f torque) { - } - - @Override - public void applyTorqueImpulse(@Nonnull Vector3f torqueImpulse) { - } - - @Override - public void clearForces() { - } - - @Override - public boolean isSensor() { - return sensor; - } - - @Override - public void setSensor(boolean sensor) { - this.sensor = sensor; - } - - @Override - public int getCollisionGroup() { - return collisionGroup; - } - - @Override - public int getCollisionMask() { - return collisionMask; - } - - @Override - public void setCollisionFilter(int group, int mask) { - collisionGroup = group; - collisionMask = mask; - } - - @Override - public boolean isContinuousCollisionEnabled() { - return continuousCollision; - } - - @Override - public void setContinuousCollisionEnabled(boolean enabled) { - continuousCollision = enabled; - } - - @Nonnull - @Override - public ShapeType getShapeType() { - return shapeType; - } - - @Nonnull - @Override - public Vector3f getBoxHalfExtents() { - return new Vector3f(halfExtents); - } - - @Override - public float getSphereRadius() { - return radius; - } - - @Override - public float getHalfHeight() { - return halfHeight; - } - - @Nonnull - @Override - public PhysicsAxis getShapeAxis() { - return axis; - } - - @Override - public float getCenterOfMassOffsetY() { - return 0.0f; - } - } - - private static final class InMemoryPhysicsJoint implements PhysicsJoint { - - private final PhysicsJointType type; - private final PhysicsBody bodyA; - private final PhysicsBody bodyB; - private final Vector3f anchorA; - private final Vector3f anchorB; - @Nullable - private final Vector3f axis; - private boolean enabled = true; - private float lowerLimit; - private float upperLimit; - private boolean motorEnabled; - private float motorTargetVelocity; - private float motorMaxForce; - - private InMemoryPhysicsJoint(@Nonnull PhysicsJointType type, - @Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nullable Vector3f axis) { - this.type = type; - this.bodyA = bodyA; - this.bodyB = bodyB; - this.anchorA = new Vector3f(anchorA); - this.anchorB = new Vector3f(anchorB); - this.axis = axis != null ? new Vector3f(axis) : null; - } - - @Nonnull - @Override - public PhysicsJointType getType() { - return type; - } - - @Nonnull - @Override - public PhysicsBody getBodyA() { - return bodyA; - } - - @Nonnull - @Override - public PhysicsBody getBodyB() { - return bodyB; - } - - @Override - public boolean isEnabled() { - return enabled; - } - - @Override - public void setEnabled(boolean enabled) { - this.enabled = enabled; - } - - @Nonnull - @Override - public Vector3f getAnchorA() { - return new Vector3f(anchorA); - } - - @Nonnull - @Override - public Vector3f getAnchorB() { - return new Vector3f(anchorB); - } - - @Nullable - @Override - public Vector3f getAxis() { - return axis != null ? new Vector3f(axis) : null; - } - - @Override - public float getLowerLimit() { - return lowerLimit; - } - - @Override - public float getUpperLimit() { - return upperLimit; - } - - @Override - public void setLimits(float lowerLimit, float upperLimit) { - this.lowerLimit = lowerLimit; - this.upperLimit = upperLimit; - } - - @Override - public boolean isMotorEnabled() { - return motorEnabled; - } - - @Override - public void setMotorEnabled(boolean enabled) { - motorEnabled = enabled; - } - - @Override - public float getMotorTargetVelocity() { - return motorTargetVelocity; - } - - @Override - public float getMotorMaxForce() { - return motorMaxForce; - } - - @Override - public void setMotor(float targetVelocity, float maxForce) { - motorTargetVelocity = targetVelocity; - motorMaxForce = maxForce; - } - } -} diff --git a/impulse-api/build.gradle.kts b/impulse-backends/api/build.gradle.kts similarity index 63% rename from impulse-api/build.gradle.kts rename to impulse-backends/api/build.gradle.kts index 25894570..7a8b5243 100644 --- a/impulse-api/build.gradle.kts +++ b/impulse-backends/api/build.gradle.kts @@ -3,6 +3,10 @@ plugins { id("java-test-fixtures") } +base { + archivesName.set("impulse-backend-api") +} + dependencies { api(libs.jsr305) api(libs.joml) @@ -12,3 +16,9 @@ dependencies { testFixturesImplementation(platform(libs.junit.bom)) testFixturesApi(libs.junit.jupiter.api) } + +tasks.named("jar") { + manifest { + attributes["Automatic-Module-Name"] = "impulse.api" + } +} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/BackendId.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/BackendId.java similarity index 80% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/BackendId.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/BackendId.java index 147097e0..60a8c65f 100644 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/BackendId.java +++ b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/BackendId.java @@ -3,9 +3,9 @@ import javax.annotation.Nonnull; /** - * PhysicsBackend Identifier to provide serialization options + * Physics backend identifier used for provider selection and persistence. * - * @param value the identifier, follows [backend_provider]:[backend_name], e.g. impulse:bullet + * @param value the identifier, follows [backend_provider]:[backend_name], e.g. impulse:rapier */ public record BackendId(@Nonnull String value) { diff --git a/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/ImpulseBackendRegistry.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/ImpulseBackendRegistry.java new file mode 100644 index 00000000..c3bde275 --- /dev/null +++ b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/ImpulseBackendRegistry.java @@ -0,0 +1,103 @@ +package dev.hytalemodding.impulse.api; + +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nonnull; + +/** + * Entry point and backend registry for Impulse. + */ +public final class ImpulseBackendRegistry { + + private static final Logger LOGGER = Logger.getLogger("Impulse"); + + private static final Object REGISTRY_LOCK = new Object(); + + private static final Map RUNTIME_PROVIDERS = + new HashMap<>(); + + private static final Map BACKEND_INIT_LOCKS = new HashMap<>(); + + private static final Set INITIALIZED_BACKENDS = new HashSet<>(); + + private ImpulseBackendRegistry() { + } + + /** + * Register or replace an id-only backend runtime provider. + */ + public static void registerRuntimeProvider(@Nonnull PhysicsBackendRuntimeProvider provider) { + synchronized (REGISTRY_LOCK) { + RUNTIME_PROVIDERS.put(provider.getId(), provider); + BACKEND_INIT_LOCKS.computeIfAbsent(provider.getId(), ignored -> new Object()); + INITIALIZED_BACKENDS.remove(provider.getId()); + } + } + + @Nonnull + public static Collection getRuntimeProviders() { + synchronized (REGISTRY_LOCK) { + return Collections.unmodifiableCollection(new ArrayList<>(RUNTIME_PROVIDERS.values())); + } + } + + @Nonnull + public static PhysicsBackendRuntimeProvider getRuntimeProvider(@Nonnull BackendId backendId) { + synchronized (REGISTRY_LOCK) { + PhysicsBackendRuntimeProvider provider = RUNTIME_PROVIDERS.get(backendId); + if (provider == null) { + throw new IllegalStateException("No backend runtime provider registered with id: " + backendId); + } + return provider; + } + } + + @Nonnull + public static PhysicsBackendRuntime createRuntime(@Nonnull BackendId backendId) { + PhysicsBackendRuntimeProvider provider = getRuntimeProvider(backendId); + ensureRuntimeProviderInitialized(backendId, provider); + return provider.createRuntime(); + } + + private static void ensureRuntimeProviderInitialized(@Nonnull BackendId backendId, + @Nonnull PhysicsBackendRuntimeProvider provider) { + Object initLock; + synchronized (REGISTRY_LOCK) { + if (INITIALIZED_BACKENDS.contains(backendId)) { + LOGGER.log(Level.FINEST, + "Physics backend runtime " + backendId + " already initialized"); + return; + } + initLock = BACKEND_INIT_LOCKS.computeIfAbsent(backendId, ignored -> new Object()); + } + + synchronized (initLock) { + synchronized (REGISTRY_LOCK) { + if (INITIALIZED_BACKENDS.contains(backendId)) { + LOGGER.log(Level.FINEST, + "Physics backend runtime " + backendId + " already initialized"); + return; + } + } + + LOGGER.log(Level.FINE, + "Initializing physics backend runtime " + backendId + " on thread " + + Thread.currentThread().getName()); + provider.init(); + + synchronized (REGISTRY_LOCK) { + INITIALIZED_BACKENDS.add(backendId); + } + LOGGER.log(Level.INFO, "Initialized physics backend runtime " + backendId); + } + } +} diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsAxis.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsAxis.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsAxis.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsAxis.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEventKind.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEventKind.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEventKind.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBackendEventKind.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBodyActivationPhase.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBodyActivationPhase.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBodyActivationPhase.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBodyActivationPhase.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBodySnapshot.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBodySnapshot.java similarity index 89% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBodySnapshot.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBodySnapshot.java index c7ea3ee5..75ab51a0 100644 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBodySnapshot.java +++ b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBodySnapshot.java @@ -7,11 +7,10 @@ import org.joml.Vector3f; /** - * Immutable copy of body state captured on the physics owner. + * Immutable copy of body state captured from live backend state. * - *

Snapshots deliberately contain shape metadata instead of a live {@link PhysicsBody} handle so - * they can be published to world-thread readers and debug systems without escaping backend - * ownership.

+ *

Snapshots deliberately contain shape metadata instead of live backend handles so they can be + * published to world-thread readers and debug systems without escaping backend ownership.

*/ public final class PhysicsBodySnapshot { @@ -302,67 +301,6 @@ public static PhysicsBodySnapshot of(float positionX, shapeAxis); } - @Nonnull - public static PhysicsBodySnapshot from(@Nonnull PhysicsBody body) { - return from(body, null); - } - - @Nonnull - public static PhysicsBodySnapshot from(@Nonnull PhysicsBody body, - @Nullable PhysicsBodySnapshot previous) { - Objects.requireNonNull(body, "body"); - boolean sleeping = body.isSleeping(); - if (sleeping && previous != null && previous.sleeping()) { - return previous; - } - - Vector3f position = new Vector3f(); - Quaternionf rotation = new Quaternionf(); - Vector3f linearVelocity = new Vector3f(); - Vector3f angularVelocity = new Vector3f(); - body.getPosition(position); - body.getRotation(rotation); - PhysicsBodyType bodyType = body.getBodyType(); - if (!sleeping && bodyType != PhysicsBodyType.STATIC) { - body.getLinearVelocity(linearVelocity); - body.getAngularVelocity(angularVelocity); - } - Vector3f boxHalfExtents = body.getBoxHalfExtents(); - return new PhysicsBodySnapshot(position.x, - position.y, - position.z, - rotation.x, - rotation.y, - rotation.z, - rotation.w, - linearVelocity.x, - linearVelocity.y, - linearVelocity.z, - angularVelocity.x, - angularVelocity.y, - angularVelocity.z, - bodyType, - sleeping, - body.isSensor(), - body.getMass(), - body.getFriction(), - body.getRestitution(), - body.getLinearDamping(), - body.getAngularDamping(), - body.getCollisionGroup(), - body.getCollisionMask(), - body.isContinuousCollisionEnabled(), - body.getCenterOfMassOffsetY(), - body.getShapeType(), - boxHalfExtents != null, - boxHalfExtents != null ? boxHalfExtents.x : 0.0f, - boxHalfExtents != null ? boxHalfExtents.y : 0.0f, - boxHalfExtents != null ? boxHalfExtents.z : 0.0f, - body.getSphereRadius(), - body.getHalfHeight(), - body.getShapeAxis()); - } - @Nonnull public Vector3f position() { return new Vector3f(positionX, positionY, positionZ); diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBodyType.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBodyType.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBodyType.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsBodyType.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsCollisionFilters.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsCollisionFilters.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsCollisionFilters.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsCollisionFilters.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsContactPhase.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsContactPhase.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsContactPhase.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsContactPhase.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsRuntimeStats.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsRuntimeStats.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsRuntimeStats.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsRuntimeStats.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsStepPhaseStats.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsStepPhaseStats.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/PhysicsStepPhaseStats.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/PhysicsStepPhaseStats.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/ShapeType.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/ShapeType.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/ShapeType.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/ShapeType.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/SpaceId.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/SpaceId.java similarity index 70% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/SpaceId.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/SpaceId.java index ae2b5bc2..e926165b 100644 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/SpaceId.java +++ b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/SpaceId.java @@ -3,15 +3,14 @@ import java.util.concurrent.atomic.AtomicInteger; /** - * PhysicsSpace identifier + * Physics space identifier. * - * @param value + * @param value backend-local numeric space id */ public record SpaceId(int value) { /* - * TODO: If this appears in profiles, replace it with a per-world allocator in - * PhysicsWorldResource to avoid a global static counter. + * PhysicsStore space helpers to avoid a global static counter. */ private static final AtomicInteger COUNTER = new AtomicInteger(0); diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsActivationTuning.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsActivationTuning.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsActivationTuning.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsActivationTuning.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilityId.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilityId.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilityId.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilityId.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsSolverTuning.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsSolverTuning.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsSolverTuning.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/capability/PhysicsSolverTuning.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendBodyIdSource.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendBodyIdSource.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendBodyIdSource.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendBodyIdSource.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendBodySnapshotSink.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendBodySnapshotSink.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendBodySnapshotSink.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendBodySnapshotSink.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendContactSink.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendContactSink.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendContactSink.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendContactSink.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendExtensionSettingsSource.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendExtensionSettingsSource.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendExtensionSettingsSource.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendExtensionSettingsSource.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendJointType.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendJointType.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendJointType.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendJointType.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendQuatSink.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendQuatSink.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendQuatSink.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendQuatSink.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendRayHitSink.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendRayHitSink.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendRayHitSink.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendRayHitSink.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendRuntimeCodes.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendRuntimeCodes.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendRuntimeCodes.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendRuntimeCodes.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendRuntimeStatsSink.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendRuntimeStatsSink.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendRuntimeStatsSink.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendRuntimeStatsSink.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendStepPhaseStatsSink.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendStepPhaseStatsSink.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendStepPhaseStatsSink.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendStepPhaseStatsSink.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendVec3Sink.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendVec3Sink.java similarity index 100% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendVec3Sink.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/BackendVec3Sink.java diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/PhysicsBackendRuntime.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/PhysicsBackendRuntime.java similarity index 61% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/PhysicsBackendRuntime.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/PhysicsBackendRuntime.java index a3892883..46270a3a 100644 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/PhysicsBackendRuntime.java +++ b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/PhysicsBackendRuntime.java @@ -7,14 +7,18 @@ import javax.annotation.Nonnull; /** - * Owner-lane backend runtime port using backend-local numeric ids and primitive payloads. + * Store tick backend runtime port using backend-local numeric ids and primitive payloads. */ -public interface PhysicsBackendRuntime { +public interface PhysicsBackendRuntime extends AutoCloseable { int createSpace(@Nonnull SpaceId requestedId); void destroySpace(int spaceId); + @Override + default void close() { + } + void step(int spaceId, float dt); void setGravity(int spaceId, float x, float y, float z); @@ -40,6 +44,70 @@ long createBody(int spaceId, float rotationZ, float rotationW); + default long createBodyWithInitialState(int spaceId, + int shapeTypeCode, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode, + float groundY, + float mass, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW, + float linearDamping, + float angularDamping, + float friction, + float restitution, + int collisionGroup, + int collisionMask, + boolean sensor, + boolean continuousCollisionEnabled) { + long bodyId = createBody(spaceId, + shapeTypeCode, + halfExtentX, + halfExtentY, + halfExtentZ, + radius, + halfHeight, + axisCode, + groundY, + mass, + bodyTypeCode, + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW); + try { + setBodyDamping(spaceId, bodyId, linearDamping, angularDamping); + setBodyFriction(spaceId, bodyId, friction); + setBodyRestitution(spaceId, bodyId, restitution); + setBodyCollisionFilter(spaceId, bodyId, collisionGroup, collisionMask); + setBodySensor(spaceId, bodyId, sensor); + if (continuousCollisionEnabled && supportsContinuousCollision(spaceId)) { + setBodyContinuousCollision(spaceId, bodyId, true); + } + return bodyId; + } catch (RuntimeException exception) { + try { + removeBody(spaceId, bodyId); + } catch (RuntimeException rollbackFailure) { + exception.addSuppressed(rollbackFailure); + } + throw exception; + } + } + boolean supportsVoxelTerrain(int spaceId); long createVoxelTerrain(int spaceId, @@ -189,6 +257,53 @@ int raycastAll(int spaceId, int contacts(int spaceId, @Nonnull BackendContactSink sink); + default int contacts(int spaceId, int maxContacts, @Nonnull BackendContactSink sink) { + if (maxContacts <= 0) { + return 0; + } + class BoundedContactSink implements BackendContactSink { + + private int count; + + @Override + public void accept(long bodyAId, + long bodyBId, + float pointAX, + float pointAY, + float pointAZ, + float pointBX, + float pointBY, + float pointBZ, + float normalBX, + float normalBY, + float normalBZ, + float distance, + float impulse) { + if (count >= maxContacts) { + return; + } + sink.accept(bodyAId, + bodyBId, + pointAX, + pointAY, + pointAZ, + pointBX, + pointBY, + pointBZ, + normalBX, + normalBY, + normalBZ, + distance, + impulse); + count++; + } + } + + BoundedContactSink bounded = new BoundedContactSink(); + contacts(spaceId, bounded); + return bounded.count; + } + int contactCount(int spaceId); void runtimeStats(int spaceId, @Nonnull BackendRuntimeStatsSink sink); diff --git a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/PhysicsBackendRuntimeProvider.java b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/PhysicsBackendRuntimeProvider.java similarity index 77% rename from impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/PhysicsBackendRuntimeProvider.java rename to impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/PhysicsBackendRuntimeProvider.java index 1a033d31..8d5fc3ce 100644 --- a/impulse-api/src/main/java/dev/hytalemodding/impulse/api/runtime/PhysicsBackendRuntimeProvider.java +++ b/impulse-backends/api/src/main/java/dev/hytalemodding/impulse/api/runtime/PhysicsBackendRuntimeProvider.java @@ -1,7 +1,6 @@ package dev.hytalemodding.impulse.api.runtime; import dev.hytalemodding.impulse.api.BackendId; -import java.util.logging.Level; import javax.annotation.Nonnull; /** @@ -15,9 +14,6 @@ public interface PhysicsBackendRuntimeProvider { default void init() { } - default void setInternalLoggingLevel(@Nonnull Level level) { - } - @Nonnull PhysicsBackendRuntime createRuntime(); } diff --git a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/BackendIdTest.java b/impulse-backends/api/src/test/java/dev/hytalemodding/impulse/api/BackendIdTest.java similarity index 100% rename from impulse-api/src/test/java/dev/hytalemodding/impulse/api/BackendIdTest.java rename to impulse-backends/api/src/test/java/dev/hytalemodding/impulse/api/BackendIdTest.java diff --git a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/ImpulseRegistryTest.java b/impulse-backends/api/src/test/java/dev/hytalemodding/impulse/api/ImpulseBackendRegistryRegistryTest.java similarity index 74% rename from impulse-api/src/test/java/dev/hytalemodding/impulse/api/ImpulseRegistryTest.java rename to impulse-backends/api/src/test/java/dev/hytalemodding/impulse/api/ImpulseBackendRegistryRegistryTest.java index c6fc5bcc..5b10f2fe 100644 --- a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/ImpulseRegistryTest.java +++ b/impulse-backends/api/src/test/java/dev/hytalemodding/impulse/api/ImpulseBackendRegistryRegistryTest.java @@ -7,7 +7,6 @@ import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; import java.util.HashSet; import java.util.List; @@ -22,7 +21,7 @@ import javax.annotation.Nonnull; import org.junit.jupiter.api.Test; -class ImpulseRegistryTest { +class ImpulseBackendRegistryRegistryTest { private static final AtomicInteger ID_COUNTER = new AtomicInteger(); private static final int CONCURRENT_RUNTIME_CREATIONS = 4; @@ -30,7 +29,7 @@ class ImpulseRegistryTest { @Test void throwsWhenRequestingUnknownRuntimeProvider() { IllegalStateException exception = assertThrows(IllegalStateException.class, - () -> Impulse.getRuntimeProvider(new BackendId(uniqueId()))); + () -> ImpulseBackendRegistry.getRuntimeProvider(new BackendId(uniqueId()))); assertTrue(exception.getMessage().startsWith("No backend runtime provider registered with id:")); } @@ -38,15 +37,15 @@ void throwsWhenRequestingUnknownRuntimeProvider() { @Test void registersRuntimeProvidersInitializesOnceAndCreatesRuntime() { CountingRuntimeProvider provider = new CountingRuntimeProvider(new BackendId(uniqueId())); - Impulse.registerRuntimeProvider(provider); + ImpulseBackendRegistry.registerRuntimeProvider(provider); - PhysicsBackendRuntime firstRuntime = Impulse.createRuntime(provider.getId()); - PhysicsBackendRuntime secondRuntime = Impulse.createRuntime(provider.getId()); + PhysicsBackendRuntime firstRuntime = ImpulseBackendRegistry.createRuntime(provider.getId()); + PhysicsBackendRuntime secondRuntime = ImpulseBackendRegistry.createRuntime(provider.getId()); - assertSame(provider, Impulse.getRuntimeProvider(provider.getId())); + assertSame(provider, ImpulseBackendRegistry.getRuntimeProvider(provider.getId())); assertEquals(1, provider.initCount()); assertEquals(2, provider.createRuntimeCount()); - assertTrue(Impulse.getRuntimeProviders().contains(provider)); + assertTrue(ImpulseBackendRegistry.getRuntimeProviders().contains(provider)); assertTrue(provider.createdRuntimes().contains(firstRuntime)); assertTrue(provider.createdRuntimes().contains(secondRuntime)); } @@ -57,21 +56,21 @@ void replacingRuntimeProviderResetsItsInitializationState() { CountingRuntimeProvider first = new CountingRuntimeProvider(backendId); CountingRuntimeProvider second = new CountingRuntimeProvider(backendId); - Impulse.registerRuntimeProvider(first); - Impulse.createRuntime(backendId); - Impulse.registerRuntimeProvider(second); - Impulse.createRuntime(backendId); + ImpulseBackendRegistry.registerRuntimeProvider(first); + ImpulseBackendRegistry.createRuntime(backendId); + ImpulseBackendRegistry.registerRuntimeProvider(second); + ImpulseBackendRegistry.createRuntime(backendId); assertEquals(1, first.initCount()); assertEquals(1, second.initCount()); assertEquals(1, second.createRuntimeCount()); - assertSame(second, Impulse.getRuntimeProvider(backendId)); + assertSame(second, ImpulseBackendRegistry.getRuntimeProvider(backendId)); } @Test void createsRuntimesConcurrentlyThroughRegistry() throws Exception { CountingRuntimeProvider provider = new CountingRuntimeProvider(new BackendId(uniqueId())); - Impulse.registerRuntimeProvider(provider); + ImpulseBackendRegistry.registerRuntimeProvider(provider); ExecutorService executor = Executors.newFixedThreadPool(CONCURRENT_RUNTIME_CREATIONS); CountDownLatch ready = new CountDownLatch(CONCURRENT_RUNTIME_CREATIONS); @@ -83,7 +82,7 @@ void createsRuntimesConcurrentlyThroughRegistry() throws Exception { .mapToObj(ignored -> executor.submit(() -> { ready.countDown(); assertTrue(start.await(5, TimeUnit.SECONDS)); - return Impulse.createRuntime(provider.getId()); + return ImpulseBackendRegistry.createRuntime(provider.getId()); })) .toList(); @@ -102,32 +101,6 @@ void createsRuntimesConcurrentlyThroughRegistry() throws Exception { } } - @Test - void defaultExplicitSpaceCreationDoesNotSilentlyDropRequestedId() { - PhysicsBackend backend = new PhysicsBackend() { - @Nonnull - @Override - public BackendId getId() { - return new BackendId(uniqueId()); - } - - @Override - public void init() { - } - - @Nonnull - @Override - public PhysicsSpace createSpace() { - return new FakePhysicsBackend(getId()).createSpace(); - } - }; - - UnsupportedOperationException failure = assertThrows(UnsupportedOperationException.class, - () -> backend.createSpace(new SpaceId(Integer.MAX_VALUE))); - - assertTrue(failure.getMessage().contains("must override createSpace(SpaceId)")); - } - private static String uniqueId() { return "test:backend-" + ID_COUNTER.incrementAndGet(); } diff --git a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/PhysicsAxisTest.java b/impulse-backends/api/src/test/java/dev/hytalemodding/impulse/api/PhysicsAxisTest.java similarity index 100% rename from impulse-api/src/test/java/dev/hytalemodding/impulse/api/PhysicsAxisTest.java rename to impulse-backends/api/src/test/java/dev/hytalemodding/impulse/api/PhysicsAxisTest.java diff --git a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/PhysicsBodySnapshotTest.java b/impulse-backends/api/src/test/java/dev/hytalemodding/impulse/api/PhysicsBodySnapshotTest.java similarity index 100% rename from impulse-api/src/test/java/dev/hytalemodding/impulse/api/PhysicsBodySnapshotTest.java rename to impulse-backends/api/src/test/java/dev/hytalemodding/impulse/api/PhysicsBodySnapshotTest.java diff --git a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilityIdTest.java b/impulse-backends/api/src/test/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilityIdTest.java similarity index 100% rename from impulse-api/src/test/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilityIdTest.java rename to impulse-backends/api/src/test/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilityIdTest.java diff --git a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilitySettingsTest.java b/impulse-backends/api/src/test/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilitySettingsTest.java similarity index 68% rename from impulse-api/src/test/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilitySettingsTest.java rename to impulse-backends/api/src/test/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilitySettingsTest.java index b3ddb5a4..2b12e430 100644 --- a/impulse-api/src/test/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilitySettingsTest.java +++ b/impulse-backends/api/src/test/java/dev/hytalemodding/impulse/api/capability/PhysicsCapabilitySettingsTest.java @@ -7,22 +7,6 @@ class PhysicsCapabilitySettingsTest { - @Test - void descriptorRejectsBlankDisplayName() { - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> new PhysicsCapabilityDescriptor(new PhysicsCapabilityId("impulse:test"), " ", "Test capability")); - - assertEquals("displayName cannot be blank", exception.getMessage()); - } - - @Test - void descriptorRejectsBlankDescription() { - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> new PhysicsCapabilityDescriptor(new PhysicsCapabilityId("impulse:test"), "Test", " ")); - - assertEquals("description cannot be blank", exception.getMessage()); - } - @Test void rejectsNonPositiveSolverIterations() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, diff --git a/impulse-api/src/testFixtures/java/dev/hytalemodding/impulse/api/testsupport/FakePhysicsBackendRuntimeProvider.java b/impulse-backends/api/src/testFixtures/java/dev/hytalemodding/impulse/api/testsupport/FakePhysicsBackendRuntimeProvider.java similarity index 87% rename from impulse-api/src/testFixtures/java/dev/hytalemodding/impulse/api/testsupport/FakePhysicsBackendRuntimeProvider.java rename to impulse-backends/api/src/testFixtures/java/dev/hytalemodding/impulse/api/testsupport/FakePhysicsBackendRuntimeProvider.java index 72427e4f..416d99ac 100644 --- a/impulse-api/src/testFixtures/java/dev/hytalemodding/impulse/api/testsupport/FakePhysicsBackendRuntimeProvider.java +++ b/impulse-backends/api/src/testFixtures/java/dev/hytalemodding/impulse/api/testsupport/FakePhysicsBackendRuntimeProvider.java @@ -102,6 +102,13 @@ public static final class FakePhysicsBackendRuntime implements PhysicsBackendRun private final FailureController failures; private long nextBodyId = 1L; private long nextJointId = 1L; + private int createBodyWithInitialStateCalls; + private int setBodyDampingCalls; + private int setBodyFrictionCalls; + private int setBodyRestitutionCalls; + private int setBodyCollisionFilterCalls; + private int setBodySensorCalls; + private int setBodyContinuousCollisionCalls; private FakePhysicsBackendRuntime(boolean continuousCollision, boolean voxelTerrain, @@ -126,6 +133,11 @@ public void destroySpace(int spaceId) { spaces.remove(spaceId); } + @Override + public void close() { + spaces.clear(); + } + @Override public void step(int spaceId, float dt) { requireSpace(spaceId); @@ -187,6 +199,64 @@ public long createBody(int spaceId, return bodyId; } + @Override + public long createBodyWithInitialState(int spaceId, + int shapeTypeCode, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode, + float groundY, + float mass, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW, + float linearDamping, + float angularDamping, + float friction, + float restitution, + int collisionGroup, + int collisionMask, + boolean sensor, + boolean continuousCollisionEnabled) { + createBodyWithInitialStateCalls++; + long bodyId = createBody(spaceId, + shapeTypeCode, + halfExtentX, + halfExtentY, + halfExtentZ, + radius, + halfHeight, + axisCode, + groundY, + mass, + bodyTypeCode, + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW); + BodyState body = requireBody(requireSpace(spaceId), bodyId); + body.linearDamping = linearDamping; + body.angularDamping = angularDamping; + body.friction = friction; + body.restitution = restitution; + body.collisionGroup = collisionGroup; + body.collisionMask = collisionMask; + body.sensor = sensor; + body.continuousCollision = continuousCollisionEnabled && supportsContinuousCollision(spaceId); + return bodyId; + } + @Override public boolean supportsVoxelTerrain(int spaceId) { requireSpace(spaceId); @@ -345,6 +415,7 @@ public int bodyTypeCode(int spaceId, long bodyId) { @Override public void setBodyDamping(int spaceId, long bodyId, float linearDamping, float angularDamping) { + setBodyDampingCalls++; BodyState body = requireBody(requireSpace(spaceId), bodyId); body.linearDamping = linearDamping; body.angularDamping = angularDamping; @@ -352,17 +423,36 @@ public void setBodyDamping(int spaceId, long bodyId, float linearDamping, float @Override public void setBodyFriction(int spaceId, long bodyId, float friction) { + setBodyFrictionCalls++; failures.maybeFailBodyFriction(); requireBody(requireSpace(spaceId), bodyId).friction = friction; } + public float bodyFriction(int spaceId, long bodyId) { + return requireBody(requireSpace(spaceId), bodyId).friction; + } + + public float bodyLinearDamping(int spaceId, long bodyId) { + return requireBody(requireSpace(spaceId), bodyId).linearDamping; + } + + public float bodyAngularDamping(int spaceId, long bodyId) { + return requireBody(requireSpace(spaceId), bodyId).angularDamping; + } + @Override public void setBodyRestitution(int spaceId, long bodyId, float restitution) { + setBodyRestitutionCalls++; requireBody(requireSpace(spaceId), bodyId).restitution = restitution; } + public float bodyRestitution(int spaceId, long bodyId) { + return requireBody(requireSpace(spaceId), bodyId).restitution; + } + @Override public void setBodyCollisionFilter(int spaceId, long bodyId, int group, int mask) { + setBodyCollisionFilterCalls++; BodyState body = requireBody(requireSpace(spaceId), bodyId); body.collisionGroup = group; body.collisionMask = mask; @@ -378,11 +468,17 @@ public int bodyCollisionMask(int spaceId, long bodyId) { @Override public void setBodySensor(int spaceId, long bodyId, boolean sensor) { + setBodySensorCalls++; requireBody(requireSpace(spaceId), bodyId).sensor = sensor; } + public boolean bodySensor(int spaceId, long bodyId) { + return requireBody(requireSpace(spaceId), bodyId).sensor; + } + @Override public void setBodyContinuousCollision(int spaceId, long bodyId, boolean enabled) { + setBodyContinuousCollisionCalls++; requireBody(requireSpace(spaceId), bodyId).continuousCollision = enabled; } @@ -589,6 +685,34 @@ public boolean hasSpace(int spaceId) { return spaces.containsKey(spaceId); } + public int createBodyWithInitialStateCalls() { + return createBodyWithInitialStateCalls; + } + + public int setBodyDampingCalls() { + return setBodyDampingCalls; + } + + public int setBodyFrictionCalls() { + return setBodyFrictionCalls; + } + + public int setBodyRestitutionCalls() { + return setBodyRestitutionCalls; + } + + public int setBodyCollisionFilterCalls() { + return setBodyCollisionFilterCalls; + } + + public int setBodySensorCalls() { + return setBodySensorCalls; + } + + public int setBodyContinuousCollisionCalls() { + return setBodyContinuousCollisionCalls; + } + private static void emitSnapshot(long bodyId, @Nonnull BodyState body, @Nonnull BackendBodySnapshotSink sink) { diff --git a/impulse-backends/jolt/README.md b/impulse-backends/jolt/README.md new file mode 100644 index 00000000..e24952a0 --- /dev/null +++ b/impulse-backends/jolt/README.md @@ -0,0 +1,109 @@ +# Impulse Jolt Backend + +`impulse-backends/jolt` is the Jolt backend provider module for Impulse. + +The backend id is `impulse:jolt`. The module is discovered through the same +`PhysicsBackendRuntimeProvider` service mechanism used by the other backend provider jars. + +## Native Strategy + +The integration strategy is to build and package a C++ `impulse_jolt` native resource through +Gradle using CMake and the standard Impulse native resource layout: + +```text +native/// +``` + +Resource names: + +- Linux: `native/linux/x86_64/libimpulse_jolt.so` +- Linux ARM64: `native/linux/arm64/libimpulse_jolt.so` +- macOS ARM64: `native/osx/arm64/libimpulse_jolt.dylib` +- Windows x86_64: `native/windows/x86_64/impulse_jolt.dll` + +Gradle properties: + +- `buildJoltNative` +- `joltCxx` +- `joltCmake` +- `joltCmakeGenerator` +- `joltPhysicsGitTag` +- `impulse.joltNativeResourceRoot` + +The default local native build uses CMake FetchContent to build upstream Jolt Physics from the +pinned tag `v5.5.0`. `joltPhysicsGitTag` can override that tag, and +`impulse.joltNativeResourceRoot` can still supply prebuilt native resources instead of building +locally. + +The Java boundary uses Java 25 Project Panama/FFM downcalls rather than JNI. The packaged native +library is expected to export these C ABI symbols: + +- `impulse_jolt_create_space` +- `impulse_jolt_destroy_space` +- `impulse_jolt_step` +- `impulse_jolt_set_gravity` +- `impulse_jolt_get_gravity` +- `impulse_jolt_create_body` +- `impulse_jolt_remove_body` +- `impulse_jolt_contains_body` +- `impulse_jolt_body_snapshot` +- `impulse_jolt_set_body_transform` +- `impulse_jolt_set_body_position` +- `impulse_jolt_set_body_velocity` +- `impulse_jolt_set_body_type` +- `impulse_jolt_set_body_damping` +- `impulse_jolt_set_body_friction` +- `impulse_jolt_set_body_restitution` +- `impulse_jolt_set_body_collision_filter` +- `impulse_jolt_set_body_sensor` +- `impulse_jolt_set_body_continuous_collision` +- `impulse_jolt_is_body_continuous_collision_enabled` +- `impulse_jolt_activate_body` +- `impulse_jolt_sleep_body` +- `impulse_jolt_apply_body_impulse` +- `impulse_jolt_apply_body_force` +- `impulse_jolt_create_joint` +- `impulse_jolt_remove_joint` +- `impulse_jolt_raycast_closest` +- `impulse_jolt_raycast_all` +- `impulse_jolt_contacts` +- `impulse_jolt_contact_count` +- `impulse_jolt_body_count` +- `impulse_jolt_joint_count` + +Space lifecycle, gravity, stepping, body lifecycle/mutation/snapshots, joint lifecycle, raycasts, +contact queries, body count, joint count, and runtime stats are wired through that ABI. +Java-assigned body and joint ids are stable within the runtime and map to opaque native handles +that wrap Jolt `BodyID` and constraint values. Query results map native body handles back to those +Java-assigned ids before calling Impulse sinks. +The current native implementation uses Jolt `PhysicsSystem`/`BodyInterface` for real rigid body +simulation, including broadphase, narrow phase, contact solving, gravity, forces, impulses, +activation, sensor state, motion quality, friction, restitution, raycasts, and dynamic bodies +resting on static collision. Contact queries are backed by a native Jolt `ContactListener` active +contact registry. + +Contact-event, voxel terrain, and advanced capability operations still fail or return explicit +unsupported results until their native paths are implemented and tested. Jolt is staged as a backend +provider jar for explicit runtime selection, but it is not production-complete until those paths and +server runtime validation pass. + +`impulse_jolt_body_snapshot` writes two output buffers: + +- float buffer, 24 entries: position xyz, rotation xyzw, linear velocity xyz, angular velocity xyz, + mass, friction, restitution, linear damping, angular damping, center-of-mass Y offset, box half + extents xyz, radius, half height +- int buffer, 9 entries: shape type code, body type code, sleeping flag, sensor flag, collision + group, collision mask, continuous-collision flag, has-box-half-extents flag, axis code + +Raycast buffers: + +- body-handle buffer: one native body handle per hit +- float buffer, 8 entries per hit: point xyz, normal xyz, fraction, distance + +Contact buffers: + +- body-handle buffer, 2 entries per contact: body A handle, body B handle +- float buffer, 11 entries per contact: point A xyz, point B xyz, normal B xyz, distance, impulse + +The contact impulse field is currently reported as `0.0` because Jolt's contact listener does not +provide the solved impulse on the query path used here. diff --git a/impulse-backends/jolt/build.gradle.kts b/impulse-backends/jolt/build.gradle.kts new file mode 100644 index 00000000..f2fb9749 --- /dev/null +++ b/impulse-backends/jolt/build.gradle.kts @@ -0,0 +1,298 @@ +import org.gradle.api.GradleException +import org.gradle.api.file.FileCollection +import org.gradle.api.file.DuplicatesStrategy +import org.gradle.api.tasks.PathSensitivity +import org.gradle.jvm.tasks.Jar + +plugins { + id("java-library") +} + +base { + archivesName.set("impulse-backend-jolt") +} + +data class JoltBackendPlatform( + val taskSuffix: String, + val archiveClassifier: String, + val resourceOs: String, + val resourceArch: String, + val libraryName: String +) { + val nativeResourcePath: String = "native/$resourceOs/$resourceArch" + val nativeResourceEntry: String = "$nativeResourcePath/$libraryName" +} + +val joltBackendPlatforms = listOf( + JoltBackendPlatform("LinuxX64", "linux-x86_64", "linux", "x86_64", "libimpulse_jolt.so"), + JoltBackendPlatform("LinuxArm64", "linux-arm64", "linux", "arm64", "libimpulse_jolt.so"), + JoltBackendPlatform("OsxArm64", "osx-arm64", "osx", "arm64", "libimpulse_jolt.dylib"), + JoltBackendPlatform("WindowsX64", "windows-x86_64", "windows", "x86_64", "impulse_jolt.dll") +) +val impulseLicenseFile = rootProject.layout.projectDirectory.file("LICENSE") +val joltPhysicsLicenseFile = rootProject.layout.projectDirectory.file("licenses/JOLT_PHYSICS_LICENSE") +val nativeResourceOs = detectNativeResourceOs() +val nativeResourceArch = detectNativeResourceArch(nativeResourceOs) +val nativeResourcePath = "native/$nativeResourceOs/$nativeResourceArch" +val nativeLibraryName = nativeLibraryNameFor(nativeResourceOs) +val nativeSourceDirectory = layout.projectDirectory.dir("src/main/cpp") +val nativeCmakeFile = nativeSourceDirectory.file("CMakeLists.txt") +val nativeSourceFiles = nativeSourceDirectory.asFileTree.matching { + include("**/*.cpp", "**/*.h") +} +val cmakeBuildDirectory = layout.buildDirectory.dir("cmake/jolt") +val nativeOutputDirectory = layout.buildDirectory.dir("native/jolt") +val nativeOutputFile = nativeOutputDirectory.map { directory -> directory.file(nativeLibraryName) } +val generatedJoltNativeResourceRoot = layout.buildDirectory.dir("generated/jolt-native") +val providedJoltNativeResourceRoot = providers.gradleProperty("impulse.joltNativeResourceRoot") +val joltNativeResourceRoot = providedJoltNativeResourceRoot + .map { path -> file(path) } + .orElse(generatedJoltNativeResourceRoot.map { directory -> directory.asFile }) +val cmakeExecutable = providers.gradleProperty("joltCmake").orElse("cmake") +val cxxCompiler = providers.gradleProperty("joltCxx") +val joltPhysicsGitTag = providers.gradleProperty("joltPhysicsGitTag").orElse("v5.5.0") +val ninjaAvailable = commandAvailable("ninja") +val cmakeGenerator = providers.gradleProperty("joltCmakeGenerator") + .orElse(if (ninjaAvailable) "Ninja" else "") +val cmakeAvailable = commandAvailable(cmakeExecutable.get()) +val gitAvailable = commandAvailable("git") +val buildNative = providers.gradleProperty("buildJoltNative") + .map { it.toBoolean() } + .orElse(cmakeAvailable && gitAvailable) + +fun commandAvailable(command: String): Boolean { + return try { + ProcessBuilder(command, "--version") + .redirectOutput(ProcessBuilder.Redirect.DISCARD) + .redirectError(ProcessBuilder.Redirect.DISCARD) + .start() + .waitFor() == 0 + } catch (_: Exception) { + false + } +} + +fun detectNativeResourceOs(): String { + val osName = System.getProperty("os.name").lowercase() + return when { + osName.contains("linux") -> "linux" + osName.contains("mac") || osName.contains("darwin") -> "osx" + osName.contains("windows") -> "windows" + else -> throw GradleException("Unsupported Jolt native packaging OS: " + + System.getProperty("os.name")) + } +} + +fun detectNativeResourceArch(resourceOs: String): String { + val osArch = System.getProperty("os.arch").lowercase() + val resourceArch = when (osArch) { + "amd64", "x86_64" -> "x86_64" + "aarch64", "arm64" -> "arm64" + else -> throw GradleException("Unsupported Jolt native packaging architecture: " + + System.getProperty("os.arch")) + } + if (resourceOs == "windows" && resourceArch != "x86_64") { + throw GradleException("Unsupported Jolt native packaging platform: " + + System.getProperty("os.name") + " " + System.getProperty("os.arch")) + } + return resourceArch +} + +fun nativeLibraryNameFor(resourceOs: String): String { + return when (resourceOs) { + "windows" -> "impulse_jolt.dll" + "osx" -> "libimpulse_jolt.dylib" + else -> "libimpulse_jolt.so" + } +} + +fun runtimeClasspathWithoutBundledApi(): FileCollection { + return configurations.runtimeClasspath.get() + .filter { file -> !file.name.startsWith("impulse-backend-api-") } +} + +fun Jar.expandRuntimeClasspath(runtimeClasspath: FileCollection) { + dependsOn(runtimeClasspath.buildDependencies) + from({ + runtimeClasspath.map { file -> if (file.isDirectory) file else zipTree(file) } + }) +} + +fun Jar.includeBackendRuntime() { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + expandRuntimeClasspath(runtimeClasspathWithoutBundledApi()) + exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA") +} + +fun Jar.includeBackendLicenseNotices() { + from(impulseLicenseFile) { + into("META-INF/licenses/impulse") + rename { "LICENSE" } + } + from(joltPhysicsLicenseFile) { + into("META-INF/licenses/jolt-physics") + rename { "LICENSE" } + } +} + +fun Jar.includeJoltNativeResource(platform: JoltBackendPlatform) { + val archiveClassifier = platform.archiveClassifier + val nativeResourcePath = platform.nativeResourcePath + val nativeResourceEntry = platform.nativeResourceEntry + + from(joltNativeResourceRoot.map { root -> + root.resolve(nativeResourcePath) + }) { + into(nativeResourcePath) + } + doFirst { + val nativeResource = joltNativeResourceRoot.get().resolve(nativeResourceEntry) + if (!nativeResource.isFile) { + throw GradleException("Missing Jolt native resource for " + + archiveClassifier + ": " + nativeResource.absolutePath + + ". Build it on a matching runner or provide -Pimpulse.joltNativeResourceRoot.") + } + } +} + +val configureJoltNative by tasks.registering(Exec::class) { + onlyIf { buildNative.get() } + + inputs.file(nativeCmakeFile) + inputs.files(nativeSourceFiles) + .withPropertyName("nativeSourceFiles") + .withPathSensitivity(PathSensitivity.RELATIVE) + inputs.property("joltPhysicsGitTag", joltPhysicsGitTag) + inputs.property("joltCmake", cmakeExecutable) + inputs.property("joltCmakeGenerator", cmakeGenerator) + inputs.property("joltCxx", cxxCompiler.orNull ?: "") + outputs.file(cmakeBuildDirectory.map { directory -> directory.file("CMakeCache.txt") }) + + doFirst { + cmakeBuildDirectory.get().asFile.mkdirs() + nativeOutputDirectory.get().asFile.mkdirs() + val command = mutableListOf( + cmakeExecutable.get(), + "-S", + nativeSourceDirectory.asFile.absolutePath, + "-B", + cmakeBuildDirectory.get().asFile.absolutePath, + "-DCMAKE_BUILD_TYPE=Release", + "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=${nativeOutputDirectory.get().asFile.absolutePath}", + "-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=${nativeOutputDirectory.get().asFile.absolutePath}", + "-DJOLT_PHYSICS_GIT_TAG=${joltPhysicsGitTag.get()}" + ) + val generator = cmakeGenerator.get().trim() + if (generator.isNotEmpty()) { + command.addAll(listOf("-G", generator)) + } + cxxCompiler.orNull?.trim() + ?.takeIf { it.isNotEmpty() } + ?.let { command.add("-DCMAKE_CXX_COMPILER=$it") } + commandLine(command) + } +} + +val compileJoltNative by tasks.registering(Exec::class) { + dependsOn(configureJoltNative) + onlyIf { buildNative.get() } + + inputs.file(nativeCmakeFile) + inputs.files(nativeSourceFiles) + .withPropertyName("nativeSourceFiles") + .withPathSensitivity(PathSensitivity.RELATIVE) + inputs.property("joltPhysicsGitTag", joltPhysicsGitTag) + inputs.property("joltCmake", cmakeExecutable) + inputs.property("joltCmakeGenerator", cmakeGenerator) + inputs.property("joltCxx", cxxCompiler.orNull ?: "") + outputs.file(nativeOutputFile) + + doFirst { + val command = mutableListOf( + cmakeExecutable.get(), + "--build", + cmakeBuildDirectory.get().asFile.absolutePath, + "--config", + "Release", + "--target", + "impulse_jolt" + ) + commandLine(command) + } +} + +val stageJoltNativeResource by tasks.registering(Copy::class) { + dependsOn(compileJoltNative) + onlyIf { buildNative.get() } + + from(nativeOutputFile) + into(generatedJoltNativeResourceRoot.map { it.dir(nativeResourcePath) }) +} + +sourceSets { + main { + resources.srcDir(joltNativeResourceRoot) + } +} + +tasks.jar { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + expandRuntimeClasspath(runtimeClasspathWithoutBundledApi()) + includeBackendLicenseNotices() + exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA") +} + +tasks.processResources { + if (!providedJoltNativeResourceRoot.isPresent) { + dependsOn(stageJoltNativeResource) + } +} + +val platformJarTasks = joltBackendPlatforms.map { platform -> + tasks.register("packageJoltBackend${platform.taskSuffix}") { + group = "build" + description = "Packages the Jolt backend provider jar for ${platform.archiveClassifier}" + archiveClassifier.set(platform.archiveClassifier) + + if (!providedJoltNativeResourceRoot.isPresent + && platform.resourceOs == nativeResourceOs + && platform.resourceArch == nativeResourceArch) { + dependsOn(stageJoltNativeResource) + } + + from(sourceSets.main.get().output) { + exclude("native/**") + } + includeBackendRuntime() + includeBackendLicenseNotices() + includeJoltNativeResource(platform) + } +} + +tasks.register("packageJoltBackendUniversal") { + group = "build" + description = "Packages the Jolt backend provider jar with every configured native library" + archiveClassifier.set("universal") + + from(sourceSets.main.get().output) { + exclude("native/**") + } + includeBackendRuntime() + includeBackendLicenseNotices() + joltBackendPlatforms.forEach { platform -> + includeJoltNativeResource(platform) + } +} + +tasks.register("packageJoltBackendPlatformJars") { + group = "build" + description = "Packages all Jolt per-platform backend jars plus the universal jar" + dependsOn(platformJarTasks) + dependsOn(tasks.named("packageJoltBackendUniversal")) +} + +dependencies { + api(project(":impulse-backends:api")) + + implementation(project(":impulse-backends:native-loader")) +} diff --git a/impulse-backends/jolt/src/main/cpp/CMakeLists.txt b/impulse-backends/jolt/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..4c0909d3 --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/CMakeLists.txt @@ -0,0 +1,91 @@ +cmake_minimum_required(VERSION 3.20 FATAL_ERROR) + +project(impulse_jolt_native LANGUAGES CXX) + +include(FetchContent) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +set(CMAKE_CXX_VISIBILITY_PRESET hidden) +set(CMAKE_VISIBILITY_INLINES_HIDDEN ON) + +set(JOLT_PHYSICS_GIT_TAG "v5.5.0" CACHE STRING "Jolt Physics git tag to build.") + +set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) +set(DOUBLE_PRECISION OFF CACHE BOOL "" FORCE) +set(GENERATE_DEBUG_SYMBOLS OFF CACHE BOOL "" FORCE) +set(OVERRIDE_CXX_FLAGS OFF CACHE BOOL "" FORCE) +set(CROSS_PLATFORM_DETERMINISTIC OFF CACHE BOOL "" FORCE) +set(INTERPROCEDURAL_OPTIMIZATION OFF CACHE BOOL "" FORCE) +set(FLOATING_POINT_EXCEPTIONS_ENABLED OFF CACHE BOOL "" FORCE) +set(CPP_EXCEPTIONS_ENABLED OFF CACHE BOOL "" FORCE) +set(CPP_RTTI_ENABLED OFF CACHE BOOL "" FORCE) +set(OBJECT_LAYER_BITS 16 CACHE STRING "" FORCE) +set(ENABLE_ALL_WARNINGS OFF CACHE BOOL "" FORCE) +set(DEBUG_RENDERER_IN_DEBUG_AND_RELEASE OFF CACHE BOOL "" FORCE) +set(PROFILER_IN_DEBUG_AND_RELEASE OFF CACHE BOOL "" FORCE) +set(ENABLE_OBJECT_STREAM OFF CACHE BOOL "" FORCE) +set(ENABLE_INSTALL OFF CACHE BOOL "" FORCE) +set(TARGET_UNIT_TESTS OFF CACHE BOOL "" FORCE) +set(TARGET_HELLO_WORLD OFF CACHE BOOL "" FORCE) +set(TARGET_PERFORMANCE_TEST OFF CACHE BOOL "" FORCE) +set(TARGET_SAMPLES OFF CACHE BOOL "" FORCE) +set(TARGET_VIEWER OFF CACHE BOOL "" FORCE) + +if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|AMD64|amd64)$") + set(USE_SSE4_1 OFF CACHE BOOL "" FORCE) + set(USE_SSE4_2 OFF CACHE BOOL "" FORCE) + set(USE_AVX OFF CACHE BOOL "" FORCE) + set(USE_AVX2 OFF CACHE BOOL "" FORCE) + set(USE_AVX512 OFF CACHE BOOL "" FORCE) + set(USE_LZCNT OFF CACHE BOOL "" FORCE) + set(USE_TZCNT OFF CACHE BOOL "" FORCE) + set(USE_F16C OFF CACHE BOOL "" FORCE) + set(USE_FMADD OFF CACHE BOOL "" FORCE) +endif() + +FetchContent_Declare( + JoltPhysics + GIT_REPOSITORY "https://github.com/jrouwe/JoltPhysics.git" + GIT_TAG "${JOLT_PHYSICS_GIT_TAG}" + GIT_SHALLOW TRUE + SOURCE_SUBDIR "Build" +) +FetchContent_MakeAvailable(JoltPhysics) + +add_library(impulse_jolt SHARED) +target_sources(impulse_jolt PRIVATE + abi/impulse_jolt_body_api.cpp + abi/impulse_jolt_joint_api.cpp + abi/impulse_jolt_query_api.cpp + abi/impulse_jolt_space_api.cpp + internal/impulse_jolt_joints.cpp + internal/impulse_jolt_query.cpp + internal/impulse_jolt_registry.cpp + internal/impulse_jolt_shapes.cpp + internal/impulse_jolt_snapshot.cpp + internal/impulse_jolt_space.cpp +) +target_include_directories(impulse_jolt PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" + "${JoltPhysics_SOURCE_DIR}/.." +) +target_link_libraries(impulse_jolt PRIVATE Jolt) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + set(IMPULSE_JOLT_EXPORT_SCRIPT "${CMAKE_CURRENT_BINARY_DIR}/impulse_jolt_exports.map") + file(WRITE "${IMPULSE_JOLT_EXPORT_SCRIPT}" "IMPULSE_JOLT_1.0 {\n global:\n impulse_jolt_*;\n local:\n *;\n};\n") + target_link_options(impulse_jolt PRIVATE + "-Wl,--exclude-libs,ALL" + "-Wl,--version-script=${IMPULSE_JOLT_EXPORT_SCRIPT}" + ) +endif() + +set_target_properties(impulse_jolt PROPERTIES + OUTPUT_NAME "impulse_jolt" + PREFIX "${CMAKE_SHARED_LIBRARY_PREFIX}" + SUFFIX "${CMAKE_SHARED_LIBRARY_SUFFIX}" + CXX_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON +) diff --git a/impulse-backends/jolt/src/main/cpp/abi/impulse_jolt_body_api.cpp b/impulse-backends/jolt/src/main/cpp/abi/impulse_jolt_body_api.cpp new file mode 100644 index 00000000..113e421c --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/abi/impulse_jolt_body_api.cpp @@ -0,0 +1,454 @@ +#include "internal/impulse_jolt_registry.h" +#include "internal/impulse_jolt_shapes.h" +#include "internal/impulse_jolt_snapshot.h" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace ImpulseJolt; + +extern "C" { + +IMPULSE_JOLT_EXPORT std::int64_t impulse_jolt_create_body(std::int64_t space_handle, + int shape_type, + float half_extent_x, + float half_extent_y, + float half_extent_z, + float radius, + float half_height, + int axis, + float ground_y, + float mass, + int body_type, + float position_x, + float position_y, + float position_z, + float rotation_x, + float rotation_y, + float rotation_z, + float rotation_w) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + if (space == nullptr) { + return 0; + } + + JPH::ShapeRefC shape = CreateShape(shape_type, + half_extent_x, + half_extent_y, + half_extent_z, + radius, + half_height, + axis, + ground_y); + if (shape == nullptr) { + return 0; + } + + BodyState body; + body.m_ShapeType = shape_type; + body.m_BodyType = body_type; + body.m_Mass = mass; + body.m_CenterOfMassOffsetY = CenterOfMassOffsetY(shape_type, + half_extent_y, + radius, + half_height, + axis); + body.m_HasBoxHalfExtents = shape_type == ShapeBox + && half_extent_x > 0.0F + && half_extent_y > 0.0F + && half_extent_z > 0.0F; + body.m_HalfExtentX = half_extent_x; + body.m_HalfExtentY = half_extent_y; + body.m_HalfExtentZ = half_extent_z; + body.m_Radius = radius; + body.m_HalfHeight = half_height; + body.m_Axis = axis; + + JPH::BodyCreationSettings settings(shape, + JPH::RVec3(position_x, position_y, position_z), + JPH::Quat(rotation_x, rotation_y, rotation_z, rotation_w), + MotionType(body_type), + ObjectLayer(body.m_CollisionGroup, body.m_CollisionMask)); + settings.mFriction = body.m_Friction; + settings.mRestitution = body.m_Restitution; + settings.mLinearDamping = body.m_LinearDamping; + settings.mAngularDamping = body.m_AngularDamping; + settings.mAllowDynamicOrKinematic = true; + settings.mIsSensor = body.m_Sensor; + if (mass > 0.0F && body_type != BodyStatic) { + settings.mOverrideMassProperties = JPH::EOverrideMassProperties::CalculateInertia; + settings.mMassPropertiesOverride.mMass = mass; + } + + JPH::BodyID BodyId = + space->m_PhysicsSystem.GetBodyInterface().CreateAndAddBody(settings, + body_type == BodyStatic + ? JPH::EActivation::DontActivate + : JPH::EActivation::Activate); + if (BodyId.IsInvalid()) { + return 0; + } + body.m_BodyId = BodyId; + + const std::uint64_t body_handle = g_NextBodyHandle++; + space->m_Bodies.emplace(body_handle, body); + space->m_BodyHandlesByJoltId.emplace(BodyId.GetIndexAndSequenceNumber(), body_handle); + return static_cast(body_handle); +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_remove_body(std::int64_t space_handle, std::int64_t body_handle) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + if (space == nullptr) { + return 0; + } + auto iterator = space->m_Bodies.find(static_cast(body_handle)); + if (iterator == space->m_Bodies.end()) { + return 1; + } + space->EraseConstraintsForBodyHandle(static_cast(body_handle)); + JPH::BodyInterface& body_interface = space->m_PhysicsSystem.GetBodyInterface(); + if (body_interface.IsAdded(iterator->second.m_BodyId)) { + body_interface.RemoveBody(iterator->second.m_BodyId); + } + body_interface.DestroyBody(iterator->second.m_BodyId); + space->m_BodyHandlesByJoltId.erase(iterator->second.m_BodyId.GetIndexAndSequenceNumber()); + space->EraseContactRecordsForBodyHandle(static_cast(body_handle)); + space->m_Bodies.erase(iterator); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_contains_body(std::int64_t space_handle, std::int64_t body_handle) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + return space != nullptr + && FindBody(*space, static_cast(body_handle)) != nullptr + ? 1 + : 0; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_body_snapshot(std::int64_t space_handle, + std::int64_t body_handle, + float* floats, + int* ints) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + if (space == nullptr || floats == nullptr || ints == nullptr) { + return 0; + } + BodyState* body = FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + WriteSnapshot(*space, *body, floats, ints); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_set_body_transform(std::int64_t space_handle, + std::int64_t body_handle, + float position_x, + float position_y, + float position_z, + float rotation_x, + float rotation_y, + float rotation_z, + float rotation_w) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + BodyState* body = space == nullptr + ? nullptr + : FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + space->m_PhysicsSystem.GetBodyInterface().SetPositionAndRotation(body->m_BodyId, + JPH::RVec3(position_x, position_y, position_z), + JPH::Quat(rotation_x, rotation_y, rotation_z, rotation_w), + JPH::EActivation::Activate); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_set_body_position(std::int64_t space_handle, + std::int64_t body_handle, + float x, + float y, + float z) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + BodyState* body = space == nullptr + ? nullptr + : FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + space->m_PhysicsSystem.GetBodyInterface().SetPosition(body->m_BodyId, + JPH::RVec3(x, y, z), + JPH::EActivation::Activate); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_set_body_velocity(std::int64_t space_handle, + std::int64_t body_handle, + float linear_x, + float linear_y, + float linear_z, + float angular_x, + float angular_y, + float angular_z) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + BodyState* body = space == nullptr + ? nullptr + : FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + space->m_PhysicsSystem.GetBodyInterface().SetLinearAndAngularVelocity(body->m_BodyId, + JPH::Vec3(linear_x, linear_y, linear_z), + JPH::Vec3(angular_x, angular_y, angular_z)); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_set_body_type(std::int64_t space_handle, std::int64_t body_handle, int body_type) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + BodyState* body = space == nullptr + ? nullptr + : FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + body->m_BodyType = body_type; + space->m_PhysicsSystem.GetBodyInterface().SetMotionType(body->m_BodyId, + MotionType(body_type), + body_type == BodyStatic ? JPH::EActivation::DontActivate : JPH::EActivation::Activate); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_set_body_damping(std::int64_t space_handle, + std::int64_t body_handle, + float linear, + float angular) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + BodyState* body = space == nullptr + ? nullptr + : FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + body->m_LinearDamping = std::max(0.0F, linear); + body->m_AngularDamping = std::max(0.0F, angular); + JPH::BodyLockWrite body_lock(space->m_PhysicsSystem.GetBodyLockInterface(), body->m_BodyId); + if (body_lock.Succeeded()) { + JPH::MotionProperties* motion_properties = + body_lock.GetBody().GetMotionPropertiesUnchecked(); + if (motion_properties != nullptr) { + motion_properties->SetLinearDamping(body->m_LinearDamping); + motion_properties->SetAngularDamping(body->m_AngularDamping); + } + } + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_set_body_friction(std::int64_t space_handle, + std::int64_t body_handle, + float friction) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + BodyState* body = space == nullptr + ? nullptr + : FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + body->m_Friction = friction; + space->m_PhysicsSystem.GetBodyInterface().SetFriction(body->m_BodyId, friction); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_set_body_restitution(std::int64_t space_handle, + std::int64_t body_handle, + float restitution) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + BodyState* body = space == nullptr + ? nullptr + : FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + body->m_Restitution = restitution; + space->m_PhysicsSystem.GetBodyInterface().SetRestitution(body->m_BodyId, restitution); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_set_body_collision_filter(std::int64_t space_handle, + std::int64_t body_handle, + int group, + int mask) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + BodyState* body = space == nullptr + ? nullptr + : FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + body->m_CollisionGroup = group; + body->m_CollisionMask = mask; + space->m_PhysicsSystem.GetBodyInterface().SetObjectLayer(body->m_BodyId, + ObjectLayer(group, mask)); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_set_body_sensor(std::int64_t space_handle, std::int64_t body_handle, int sensor) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + BodyState* body = space == nullptr + ? nullptr + : FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + body->m_Sensor = sensor != 0; + space->m_PhysicsSystem.GetBodyInterface().SetIsSensor(body->m_BodyId, body->m_Sensor); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_set_body_continuous_collision(std::int64_t space_handle, + std::int64_t body_handle, + int enabled) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + BodyState* body = space == nullptr + ? nullptr + : FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + body->m_ContinuousCollision = enabled != 0; + space->m_PhysicsSystem.GetBodyInterface().SetMotionQuality(body->m_BodyId, + body->m_ContinuousCollision + ? JPH::EMotionQuality::LinearCast + : JPH::EMotionQuality::Discrete); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_is_body_continuous_collision_enabled(std::int64_t space_handle, + std::int64_t body_handle) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + BodyState* body = space == nullptr + ? nullptr + : FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + return space->m_PhysicsSystem.GetBodyInterface().GetMotionQuality(body->m_BodyId) + == JPH::EMotionQuality::LinearCast + ? 1 + : 0; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_activate_body(std::int64_t space_handle, std::int64_t body_handle) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + BodyState* body = space == nullptr + ? nullptr + : FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + space->m_PhysicsSystem.GetBodyInterface().ActivateBody(body->m_BodyId); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_sleep_body(std::int64_t space_handle, std::int64_t body_handle) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + BodyState* body = space == nullptr + ? nullptr + : FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + space->m_PhysicsSystem.GetBodyInterface().DeactivateBody(body->m_BodyId); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_apply_body_impulse(std::int64_t space_handle, + std::int64_t body_handle, + float x, + float y, + float z, + int has_offset, + float offset_x, + float offset_y, + float offset_z, + int torque) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + BodyState* body = space == nullptr + ? nullptr + : FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + JPH::BodyInterface& body_interface = space->m_PhysicsSystem.GetBodyInterface(); + JPH::Vec3 value(x, y, z); + if (torque != 0) { + body_interface.AddAngularImpulse(body->m_BodyId, value); + } else if (has_offset != 0) { + body_interface.AddImpulse(body->m_BodyId, + value, + JPH::RVec3(offset_x, offset_y, offset_z)); + } else { + body_interface.AddImpulse(body->m_BodyId, value); + } + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_apply_body_force(std::int64_t space_handle, + std::int64_t body_handle, + float x, + float y, + float z, + int has_offset, + float offset_x, + float offset_y, + float offset_z, + int torque) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + BodyState* body = space == nullptr + ? nullptr + : FindBody(*space, static_cast(body_handle)); + if (body == nullptr) { + return 0; + } + JPH::BodyInterface& body_interface = space->m_PhysicsSystem.GetBodyInterface(); + JPH::Vec3 value(x, y, z); + if (torque != 0) { + body_interface.AddTorque(body->m_BodyId, value, JPH::EActivation::Activate); + } else if (has_offset != 0) { + body_interface.AddForce(body->m_BodyId, + value, + JPH::RVec3(offset_x, offset_y, offset_z), + JPH::EActivation::Activate); + } else { + body_interface.AddForce(body->m_BodyId, value, JPH::EActivation::Activate); + } + return 1; +} + +} // extern "C" diff --git a/impulse-backends/jolt/src/main/cpp/abi/impulse_jolt_joint_api.cpp b/impulse-backends/jolt/src/main/cpp/abi/impulse_jolt_joint_api.cpp new file mode 100644 index 00000000..9dd75774 --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/abi/impulse_jolt_joint_api.cpp @@ -0,0 +1,106 @@ +#include "internal/impulse_jolt_joints.h" +#include "internal/impulse_jolt_registry.h" + +#include +#include +#include + +#include +#include + +using namespace ImpulseJolt; + +extern "C" { + +IMPULSE_JOLT_EXPORT std::int64_t impulse_jolt_create_joint(std::int64_t space_handle, + int joint_type, + std::int64_t body_a_handle, + std::int64_t body_b_handle, + float anchor_ax, + float anchor_ay, + float anchor_az, + float anchor_bx, + float anchor_by, + float anchor_bz, + float axis_x, + float axis_y, + float axis_z, + float rest_length, + float stiffness, + float damping, + float lower_limit, + float upper_limit, + int motor_enabled, + float motor_target_velocity, + float motor_max_force) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + if (space == nullptr || body_a_handle == body_b_handle) { + return 0; + } + BodyState* body_a = FindBody(*space, static_cast(body_a_handle)); + BodyState* body_b = FindBody(*space, static_cast(body_b_handle)); + if (body_a == nullptr || body_b == nullptr) { + return 0; + } + + JPH::BodyID body_ids[] = {body_a->m_BodyId, body_b->m_BodyId}; + JPH::BodyLockMultiWrite body_locks(space->m_PhysicsSystem.GetBodyLockInterface(), + body_ids, + 2); + JPH::Body* locked_body_a = body_locks.GetBody(0); + JPH::Body* locked_body_b = body_locks.GetBody(1); + if (locked_body_a == nullptr || locked_body_b == nullptr) { + return 0; + } + + JPH::Ref Constraint = CreateJointConstraint(joint_type, + *locked_body_a, + *locked_body_b, + anchor_ax, + anchor_ay, + anchor_az, + anchor_bx, + anchor_by, + anchor_bz, + axis_x, + axis_y, + axis_z, + rest_length, + stiffness, + damping, + lower_limit, + upper_limit, + motor_enabled, + motor_target_velocity, + motor_max_force); + if (Constraint == nullptr) { + return 0; + } + body_locks.ReleaseLocks(); + + space->m_PhysicsSystem.AddConstraint(Constraint); + space->m_PhysicsSystem.GetBodyInterface().ActivateBody(body_a->m_BodyId); + space->m_PhysicsSystem.GetBodyInterface().ActivateBody(body_b->m_BodyId); + + const std::uint64_t joint_handle = g_NextJointHandle++; + JointState joint; + joint.m_Constraint = Constraint; + joint.m_BodyAHandle = static_cast(body_a_handle); + joint.m_BodyBHandle = static_cast(body_b_handle); + joint.m_JointType = joint_type; + space->m_Joints.emplace(joint_handle, joint); + return static_cast(joint_handle); +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_remove_joint(std::int64_t space_handle, std::int64_t joint_handle) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + if (space == nullptr) { + return 0; + } + space->RemoveJointHandle(static_cast(joint_handle)); + return 1; +} + +} // extern "C" diff --git a/impulse-backends/jolt/src/main/cpp/abi/impulse_jolt_native.h b/impulse-backends/jolt/src/main/cpp/abi/impulse_jolt_native.h new file mode 100644 index 00000000..cb45c1b5 --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/abi/impulse_jolt_native.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +#if defined(_WIN32) +#define IMPULSE_JOLT_EXPORT __declspec(dllexport) +#else +#define IMPULSE_JOLT_EXPORT __attribute__((visibility("default"))) +#endif + +namespace ImpulseJolt { + +inline constexpr int ShapeBox = 1; +inline constexpr int ShapeSphere = 2; +inline constexpr int ShapeCapsule = 3; +inline constexpr int ShapeCylinder = 4; +inline constexpr int ShapeCone = 5; +inline constexpr int ShapePlane = 6; + +inline constexpr int BodyStatic = 1; +inline constexpr int BodyDynamic = 2; +inline constexpr int BodyKinematic = 3; + +inline constexpr int JointFixed = 1; +inline constexpr int JointPoint = 2; +inline constexpr int JointHinge = 3; +inline constexpr int JointSlider = 4; +inline constexpr int JointSpring = 5; + +inline constexpr int AxisX = 1; +inline constexpr int AxisY = 2; +inline constexpr int AxisZ = 3; + +inline constexpr float MinShapeSize = 0.001F; +inline constexpr float MinAxisLengthSquared = 1.0e-6F; +inline constexpr std::uint32_t MaxBodies = 131072; +inline constexpr std::uint32_t MaxBodyPairs = 65536; +inline constexpr std::uint32_t MaxContactConstraints = 10240; +inline constexpr std::uint32_t DefaultCollisionGroup = 1; +inline constexpr int RayHitFloatCount = 8; +inline constexpr int ContactBodyHandleCount = 2; +inline constexpr int ContactFloatCount = 11; + +} // namespace ImpulseJolt diff --git a/impulse-backends/jolt/src/main/cpp/abi/impulse_jolt_query_api.cpp b/impulse-backends/jolt/src/main/cpp/abi/impulse_jolt_query_api.cpp new file mode 100644 index 00000000..6da7c5ab --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/abi/impulse_jolt_query_api.cpp @@ -0,0 +1,130 @@ +#include "internal/impulse_jolt_query.h" +#include "internal/impulse_jolt_registry.h" + +#include +#include +#include +#include +#include + +#include +#include + +using namespace ImpulseJolt; + +extern "C" { + +IMPULSE_JOLT_EXPORT int impulse_jolt_raycast_closest(std::int64_t space_handle, + float from_x, + float from_y, + float from_z, + float to_x, + float to_y, + float to_z, + std::int64_t* body_handles, + float* hits) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + if (space == nullptr || body_handles == nullptr || hits == nullptr) { + return 0; + } + + JPH::Vec3 direction(to_x - from_x, to_y - from_y, to_z - from_z); + if (direction.LengthSq() <= 0.0F) { + return 0; + } + JPH::RRayCast ray(JPH::RVec3(from_x, from_y, from_z), direction); + JPH::RayCastResult hit; + if (!space->m_PhysicsSystem.GetNarrowPhaseQuery().CastRay(ray, hit)) { + return 0; + } + return WriteRayHit(*space, ray, hit, 0, body_handles, hits) ? 1 : 0; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_raycast_all(std::int64_t space_handle, + float from_x, + float from_y, + float from_z, + float to_x, + float to_y, + float to_z, + int max_hits, + std::int64_t* body_handles, + float* hits) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + if (space == nullptr || max_hits <= 0 || body_handles == nullptr || hits == nullptr) { + return 0; + } + + JPH::Vec3 direction(to_x - from_x, to_y - from_y, to_z - from_z); + if (direction.LengthSq() <= 0.0F) { + return 0; + } + + JPH::RRayCast ray(JPH::RVec3(from_x, from_y, from_z), direction); + JPH::RayCastSettings settings; + JPH::ClosestHitPerBodyCollisionCollector collector; + space->m_PhysicsSystem.GetNarrowPhaseQuery().CastRay(ray, settings, collector); + collector.Sort(); + + int emitted = 0; + for (const JPH::RayCastResult& hit : collector.mHits) { + if (emitted >= max_hits) { + break; + } + if (WriteRayHit(*space, ray, hit, emitted, body_handles, hits)) { + emitted++; + } + } + return emitted; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_contacts(std::int64_t space_handle, + int max_contacts, + std::int64_t* body_handles, + float* contacts) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + if (space == nullptr || max_contacts <= 0 || body_handles == nullptr || contacts == nullptr) { + return 0; + } + + std::lock_guard contact_lock(space->m_ContactMutex); + int emitted = 0; + for (const ContactRecord& contact : space->m_Contacts) { + if (emitted >= max_contacts) { + break; + } + int body_offset = emitted * ContactBodyHandleCount; + body_handles[body_offset] = static_cast(contact.m_BodyAHandle); + body_handles[body_offset + 1] = static_cast(contact.m_BodyBHandle); + + int contact_offset = emitted * ContactFloatCount; + contacts[contact_offset] = contact.m_PointAX; + contacts[contact_offset + 1] = contact.m_PointAY; + contacts[contact_offset + 2] = contact.m_PointAZ; + contacts[contact_offset + 3] = contact.m_PointBX; + contacts[contact_offset + 4] = contact.m_PointBY; + contacts[contact_offset + 5] = contact.m_PointBZ; + contacts[contact_offset + 6] = contact.m_NormalBX; + contacts[contact_offset + 7] = contact.m_NormalBY; + contacts[contact_offset + 8] = contact.m_NormalBZ; + contacts[contact_offset + 9] = contact.m_Distance; + contacts[contact_offset + 10] = contact.m_Impulse; + emitted++; + } + return emitted; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_contact_count(std::int64_t space_handle) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + if (space == nullptr) { + return 0; + } + std::lock_guard contact_lock(space->m_ContactMutex); + return static_cast(space->m_Contacts.size()); +} + +} // extern "C" diff --git a/impulse-backends/jolt/src/main/cpp/abi/impulse_jolt_space_api.cpp b/impulse-backends/jolt/src/main/cpp/abi/impulse_jolt_space_api.cpp new file mode 100644 index 00000000..df897a3e --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/abi/impulse_jolt_space_api.cpp @@ -0,0 +1,75 @@ +#include "internal/impulse_jolt_registry.h" + +#include +#include +#include +#include +#include + +using namespace ImpulseJolt; + +extern "C" { + +IMPULSE_JOLT_EXPORT std::int64_t impulse_jolt_create_space() { + EnsureJoltInitialized(); + std::lock_guard lock(g_RegistryMutex); + const std::uint64_t handle = g_NextSpaceHandle++; + g_Spaces.emplace(handle, std::make_unique()); + return static_cast(handle); +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_destroy_space(std::int64_t space_handle) { + std::lock_guard lock(g_RegistryMutex); + return g_Spaces.erase(static_cast(space_handle)) > 0 ? 1 : 0; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_step(std::int64_t space_handle, float dt) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + if (space == nullptr || !std::isfinite(dt) || dt <= 0.0F) { + return 0; + } + const int collision_steps = std::max(1, static_cast(std::ceil(dt * 60.0F))); + space->m_PhysicsSystem.Update(dt, + collision_steps, + &space->m_TempAllocator, + &space->m_JobSystem); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_set_gravity(std::int64_t space_handle, float x, float y, float z) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + if (space == nullptr) { + return 0; + } + space->m_PhysicsSystem.SetGravity(JPH::Vec3(x, y, z)); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_get_gravity(std::int64_t space_handle, float* out) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + if (space == nullptr || out == nullptr) { + return 0; + } + JPH::Vec3 gravity = space->m_PhysicsSystem.GetGravity(); + out[0] = gravity.GetX(); + out[1] = gravity.GetY(); + out[2] = gravity.GetZ(); + return 1; +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_body_count(std::int64_t space_handle) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + return space == nullptr ? 0 : static_cast(space->m_Bodies.size()); +} + +IMPULSE_JOLT_EXPORT int impulse_jolt_joint_count(std::int64_t space_handle) { + std::lock_guard lock(g_RegistryMutex); + Space* space = FindSpace(static_cast(space_handle)); + return space == nullptr ? 0 : static_cast(space->m_Joints.size()); +} + +} // extern "C" diff --git a/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_joints.cpp b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_joints.cpp new file mode 100644 index 00000000..a1f2457c --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_joints.cpp @@ -0,0 +1,171 @@ +#include "internal/impulse_jolt_joints.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace ImpulseJolt { + +float FiniteOr(float value, float fallback) { + return std::isfinite(value) ? value : fallback; +} + +float NonNegative(float value) { + return std::max(0.0F, FiniteOr(value, 0.0F)); +} + +JPH::RVec3 LocalPoint(float x, float y, float z) { + return JPH::RVec3(FiniteOr(x, 0.0F), FiniteOr(y, 0.0F), FiniteOr(z, 0.0F)); +} + +JPH::Vec3 NormalizedAxis(float x, float y, float z) { + x = FiniteOr(x, 0.0F); + y = FiniteOr(y, 1.0F); + z = FiniteOr(z, 0.0F); + const float lengthSquared = x * x + y * y + z * z; + if (!std::isfinite(lengthSquared) || lengthSquared <= MinAxisLengthSquared) { + return JPH::Vec3::sAxisY(); + } + const float inverseLength = 1.0F / std::sqrt(lengthSquared); + return JPH::Vec3(x * inverseLength, y * inverseLength, z * inverseLength); +} + +JPH::Vec3 NormalForAxis(JPH::Vec3Arg axis) { + const JPH::Vec3 reference = std::fabs(axis.GetY()) < 0.9F + ? JPH::Vec3::sAxisY() + : JPH::Vec3::sAxisX(); + JPH::Vec3 normal = axis.Cross(reference); + if (normal.LengthSq() <= MinAxisLengthSquared) { + normal = axis.Cross(JPH::Vec3::sAxisZ()); + } + return normal.LengthSq() <= MinAxisLengthSquared + ? JPH::Vec3::sAxisX() + : normal.Normalized(); +} + +void ConfigureSpring(JPH::SpringSettings& settings, float stiffness, float damping) { + const float clampedStiffness = NonNegative(stiffness); + if (clampedStiffness <= 0.0F) { + return; + } + settings.mMode = JPH::ESpringMode::StiffnessAndDamping; + settings.mStiffness = clampedStiffness; + settings.mDamping = NonNegative(damping); +} + +JPH::Ref CreateJointConstraint(int jointType, + JPH::Body& bodyA, + JPH::Body& bodyB, + float anchorAX, + float anchorAY, + float anchorAZ, + float anchorBX, + float anchorBY, + float anchorBZ, + float axisXValue, + float axisYValue, + float axisZValue, + float restLength, + float stiffness, + float damping, + float lowerLimit, + float upperLimit, + int motorEnabled, + float motorTargetVelocity, + float motorMaxForce) { + const JPH::RVec3 anchorA = LocalPoint(anchorAX, anchorAY, anchorAZ); + const JPH::RVec3 anchorB = LocalPoint(anchorBX, anchorBY, anchorBZ); + const JPH::Vec3 axis = NormalizedAxis(axisXValue, axisYValue, axisZValue); + const JPH::Vec3 normal = NormalForAxis(axis); + const float orderedLower = std::min(FiniteOr(lowerLimit, 0.0F), + FiniteOr(upperLimit, 0.0F)); + const float orderedUpper = std::max(FiniteOr(lowerLimit, 0.0F), + FiniteOr(upperLimit, 0.0F)); + const bool explicitLimits = orderedLower < orderedUpper; + + switch (jointType) { + case JointFixed: { + JPH::FixedConstraintSettings settings; + settings.mSpace = JPH::EConstraintSpace::LocalToBodyCOM; + settings.mAutoDetectPoint = false; + settings.mPoint1 = anchorA; + settings.mPoint2 = anchorB; + return settings.Create(bodyA, bodyB); + } + case JointPoint: { + JPH::PointConstraintSettings settings; + settings.mSpace = JPH::EConstraintSpace::LocalToBodyCOM; + settings.mPoint1 = anchorA; + settings.mPoint2 = anchorB; + return settings.Create(bodyA, bodyB); + } + case JointHinge: { + JPH::HingeConstraintSettings settings; + settings.mSpace = JPH::EConstraintSpace::LocalToBodyCOM; + settings.mPoint1 = anchorA; + settings.mPoint2 = anchorB; + settings.mHingeAxis1 = settings.mHingeAxis2 = axis; + settings.mNormalAxis1 = settings.mNormalAxis2 = normal; + if (explicitLimits) { + settings.mLimitsMin = std::clamp(orderedLower, -JPH::JPH_PI, 0.0F); + settings.mLimitsMax = std::clamp(orderedUpper, 0.0F, JPH::JPH_PI); + } + JPH::HingeConstraint* constraint = + static_cast(settings.Create(bodyA, bodyB)); + if (constraint != nullptr && motorEnabled != 0) { + if (motorMaxForce > 0.0F) { + constraint->GetMotorSettings().SetTorqueLimit(motorMaxForce); + } + constraint->SetMotorState(JPH::EMotorState::Velocity); + constraint->SetTargetAngularVelocity(FiniteOr(motorTargetVelocity, 0.0F)); + } + return constraint; + } + case JointSlider: { + JPH::SliderConstraintSettings settings; + settings.mSpace = JPH::EConstraintSpace::LocalToBodyCOM; + settings.mAutoDetectPoint = false; + settings.mPoint1 = anchorA; + settings.mPoint2 = anchorB; + settings.mSliderAxis1 = settings.mSliderAxis2 = axis; + settings.mNormalAxis1 = settings.mNormalAxis2 = normal; + if (explicitLimits) { + settings.mLimitsMin = orderedLower; + settings.mLimitsMax = orderedUpper; + } + JPH::SliderConstraint* constraint = + static_cast(settings.Create(bodyA, bodyB)); + if (constraint != nullptr && motorEnabled != 0) { + if (motorMaxForce > 0.0F) { + constraint->GetMotorSettings().SetForceLimit(motorMaxForce); + } + constraint->SetMotorState(JPH::EMotorState::Velocity); + constraint->SetTargetVelocity(FiniteOr(motorTargetVelocity, 0.0F)); + } + return constraint; + } + case JointSpring: { + JPH::DistanceConstraintSettings settings; + settings.mSpace = JPH::EConstraintSpace::LocalToBodyCOM; + settings.mPoint1 = anchorA; + settings.mPoint2 = anchorB; + const float rest = NonNegative(restLength); + if (rest > 0.0F) { + settings.mMinDistance = rest; + settings.mMaxDistance = rest; + } + ConfigureSpring(settings.mLimitsSpringSettings, stiffness, damping); + return settings.Create(bodyA, bodyB); + } + default: + return nullptr; + } +} + +} // namespace ImpulseJolt diff --git a/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_joints.h b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_joints.h new file mode 100644 index 00000000..32509a72 --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_joints.h @@ -0,0 +1,31 @@ +#pragma once + +#include "internal/impulse_jolt_space.h" + +#include +#include + +namespace ImpulseJolt { + +JPH::Ref CreateJointConstraint(int jointType, + JPH::Body& bodyA, + JPH::Body& bodyB, + float anchorAX, + float anchorAY, + float anchorAZ, + float anchorBX, + float anchorBY, + float anchorBZ, + float axisXValue, + float axisYValue, + float axisZValue, + float restLength, + float stiffness, + float damping, + float lowerLimit, + float upperLimit, + int motorEnabled, + float motorTargetVelocity, + float motorMaxForce); + +} // namespace ImpulseJolt diff --git a/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_query.cpp b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_query.cpp new file mode 100644 index 00000000..fd513194 --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_query.cpp @@ -0,0 +1,43 @@ +#include "internal/impulse_jolt_query.h" +#include "internal/impulse_jolt_registry.h" + +#include +#include +#include + +#include + +namespace ImpulseJolt { + +bool WriteRayHit(Space& targetSpace, + const JPH::RRayCast& ray, + const JPH::RayCastResult& hit, + int index, + std::int64_t* bodyHandles, + float* hits) { + std::uint64_t bodyHandle = NativeHandleForBodyId(targetSpace, hit.mBodyID); + if (bodyHandle == 0) { + return false; + } + + JPH::RVec3 point = ray.GetPointOnRay(hit.mFraction); + JPH::Vec3 normal = JPH::Vec3::sZero(); + JPH::BodyLockRead lock(targetSpace.m_PhysicsSystem.GetBodyLockInterface(), hit.mBodyID); + if (lock.Succeeded()) { + normal = lock.GetBody().GetWorldSpaceSurfaceNormal(hit.mSubShapeID2, point); + } + + bodyHandles[index] = static_cast(bodyHandle); + int offset = index * RayHitFloatCount; + hits[offset] = static_cast(point.GetX()); + hits[offset + 1] = static_cast(point.GetY()); + hits[offset + 2] = static_cast(point.GetZ()); + hits[offset + 3] = normal.GetX(); + hits[offset + 4] = normal.GetY(); + hits[offset + 5] = normal.GetZ(); + hits[offset + 6] = hit.mFraction; + hits[offset + 7] = ray.mDirection.Length() * hit.mFraction; + return true; +} + +} // namespace ImpulseJolt diff --git a/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_query.h b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_query.h new file mode 100644 index 00000000..b0d5ffa8 --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_query.h @@ -0,0 +1,18 @@ +#pragma once + +#include "internal/impulse_jolt_space.h" + +#include + +#include + +namespace ImpulseJolt { + +bool WriteRayHit(Space& targetSpace, + const JPH::RRayCast& ray, + const JPH::RayCastResult& hit, + int index, + std::int64_t* bodyHandles, + float* hits); + +} // namespace ImpulseJolt diff --git a/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_registry.cpp b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_registry.cpp new file mode 100644 index 00000000..13601805 --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_registry.cpp @@ -0,0 +1,45 @@ +#include "internal/impulse_jolt_registry.h" + +#include +#include +#include + +#include +#include +#include + +namespace ImpulseJolt { + +std::once_flag s_JoltInitOnce; +std::mutex g_RegistryMutex; +std::uint64_t g_NextSpaceHandle = 1; +std::uint64_t g_NextBodyHandle = 1001; +std::uint64_t g_NextJointHandle = 2001; +std::unordered_map> g_Spaces; + +void EnsureJoltInitialized() { + std::call_once(s_JoltInitOnce, [] { + JPH::RegisterDefaultAllocator(); + if (JPH::Factory::sInstance == nullptr) { + JPH::Factory::sInstance = new JPH::Factory(); + } + JPH::RegisterTypes(); + }); +} + +Space* FindSpace(std::uint64_t handle) { + auto iterator = g_Spaces.find(handle); + return iterator == g_Spaces.end() ? nullptr : iterator->second.get(); +} + +BodyState* FindBody(Space& targetSpace, std::uint64_t handle) { + auto iterator = targetSpace.m_Bodies.find(handle); + return iterator == targetSpace.m_Bodies.end() ? nullptr : &iterator->second; +} + +std::uint64_t NativeHandleForBodyId(const Space& targetSpace, const JPH::BodyID& bodyId) { + auto iterator = targetSpace.m_BodyHandlesByJoltId.find(bodyId.GetIndexAndSequenceNumber()); + return iterator == targetSpace.m_BodyHandlesByJoltId.end() ? 0 : iterator->second; +} + +} // namespace ImpulseJolt diff --git a/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_registry.h b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_registry.h new file mode 100644 index 00000000..2dc95137 --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_registry.h @@ -0,0 +1,23 @@ +#pragma once + +#include "internal/impulse_jolt_space.h" + +#include +#include +#include +#include + +namespace ImpulseJolt { + +extern std::mutex g_RegistryMutex; +extern std::uint64_t g_NextSpaceHandle; +extern std::uint64_t g_NextBodyHandle; +extern std::uint64_t g_NextJointHandle; +extern std::unordered_map> g_Spaces; + +void EnsureJoltInitialized(); +Space* FindSpace(std::uint64_t handle); +BodyState* FindBody(Space& targetSpace, std::uint64_t handle); +std::uint64_t NativeHandleForBodyId(const Space& targetSpace, const JPH::BodyID& bodyId); + +} // namespace ImpulseJolt diff --git a/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_shapes.cpp b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_shapes.cpp new file mode 100644 index 00000000..ee1ea80d --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_shapes.cpp @@ -0,0 +1,128 @@ +#include "internal/impulse_jolt_shapes.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace ImpulseJolt { + +float Positive(float value) { + return std::max(value, MinShapeSize); +} + +JPH::EMotionType MotionType(int bodyType) { + switch (bodyType) { + case BodyStatic: + return JPH::EMotionType::Static; + case BodyKinematic: + return JPH::EMotionType::Kinematic; + default: + return JPH::EMotionType::Dynamic; + } +} + +JPH::ObjectLayer ObjectLayer(int collisionGroup, int collisionMask) { + constexpr std::uint32_t maskBits = JPH::ObjectLayerPairFilterMask::cMask; + std::uint32_t group = static_cast(collisionGroup) & maskBits; + std::uint32_t mask = static_cast(collisionMask) & maskBits; + if (group == 0) { + group = DefaultCollisionGroup; + } + if (mask == 0) { + mask = maskBits; + } + return JPH::ObjectLayerPairFilterMask::sGetObjectLayer(group, mask); +} + +float CenterOfMassOffsetY(int shapeType, + float halfExtentY, + float radius, + float halfHeight, + int axis) { + switch (shapeType) { + case ShapeBox: + return halfExtentY; + case ShapeSphere: + return radius; + case ShapeCapsule: + return axis == AxisY ? radius + halfHeight : radius; + case ShapeCylinder: + return axis == AxisY ? halfHeight : radius; + case ShapeCone: + return axis == AxisY ? halfHeight : radius; + default: + return 0.0F; + } +} + +JPH::Quat AxisRotation(int axis) { + switch (axis) { + case AxisX: + return JPH::Quat::sRotation(JPH::Vec3::sAxisZ(), -0.5F * JPH::JPH_PI); + case AxisZ: + return JPH::Quat::sRotation(JPH::Vec3::sAxisX(), 0.5F * JPH::JPH_PI); + default: + return JPH::Quat::sIdentity(); + } +} + +JPH::ShapeRefC RotatedForAxis(JPH::ShapeRefC shape, int axis) { + if (axis == AxisY || shape == nullptr) { + return shape; + } + return new JPH::RotatedTranslatedShape(JPH::Vec3::sZero(), AxisRotation(axis), shape); +} + +JPH::ShapeRefC CreateShape(int shapeType, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axis, + float groundY) { + switch (shapeType) { + case ShapeBox: + return new JPH::BoxShape(JPH::Vec3(Positive(halfExtentX), + Positive(halfExtentY), + Positive(halfExtentZ))); + case ShapeSphere: + return new JPH::SphereShape(Positive(radius)); + case ShapeCapsule: + return RotatedForAxis( + new JPH::CapsuleShape(Positive(halfHeight), Positive(radius)), + axis); + case ShapeCylinder: + return RotatedForAxis( + new JPH::CylinderShape(Positive(halfHeight), Positive(radius)), + axis); + case ShapeCone: { + JPH::TaperedCylinderShapeSettings settings(Positive(halfHeight), + 0.0F, + Positive(radius)); + JPH::Shape::ShapeResult result = settings.Create(); + if (result.HasError()) { + return nullptr; + } + return RotatedForAxis(result.Get(), axis); + } + case ShapePlane: + return new JPH::PlaneShape( + JPH::Plane::sFromPointAndNormal(JPH::Vec3(0.0F, groundY, 0.0F), + JPH::Vec3::sAxisY())); + default: + return nullptr; + } +} + +} // namespace ImpulseJolt diff --git a/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_shapes.h b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_shapes.h new file mode 100644 index 00000000..93399060 --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_shapes.h @@ -0,0 +1,26 @@ +#pragma once + +#include "internal/impulse_jolt_space.h" + +#include +#include + +namespace ImpulseJolt { + +JPH::EMotionType MotionType(int bodyType); +JPH::ObjectLayer ObjectLayer(int collisionGroup, int collisionMask); +float CenterOfMassOffsetY(int shapeType, + float halfExtentY, + float radius, + float halfHeight, + int axis); +JPH::ShapeRefC CreateShape(int shapeType, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axis, + float groundY); + +} // namespace ImpulseJolt diff --git a/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_snapshot.cpp b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_snapshot.cpp new file mode 100644 index 00000000..0ea9befc --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_snapshot.cpp @@ -0,0 +1,72 @@ +#include "internal/impulse_jolt_snapshot.h" + +#include +#include +#include +#include +#include +#include + +namespace ImpulseJolt { + +void WriteSnapshot(Space& targetSpace, const BodyState& body, float* floats, int* ints) { + JPH::BodyInterface& bodyInterface = targetSpace.m_PhysicsSystem.GetBodyInterface(); + JPH::RVec3 position = JPH::RVec3::sZero(); + JPH::Quat rotation = JPH::Quat::sIdentity(); + JPH::Vec3 linearVelocity = JPH::Vec3::sZero(); + JPH::Vec3 angularVelocity = JPH::Vec3::sZero(); + float linearDamping = body.m_LinearDamping; + float angularDamping = body.m_AngularDamping; + + bodyInterface.GetPositionAndRotation(body.m_BodyId, position, rotation); + bodyInterface.GetLinearAndAngularVelocity(body.m_BodyId, linearVelocity, angularVelocity); + JPH::BodyLockRead lock(targetSpace.m_PhysicsSystem.GetBodyLockInterface(), body.m_BodyId); + if (lock.Succeeded()) { + const JPH::MotionProperties* motionProperties = + lock.GetBody().GetMotionPropertiesUnchecked(); + if (motionProperties != nullptr) { + linearDamping = motionProperties->GetLinearDamping(); + angularDamping = motionProperties->GetAngularDamping(); + } + } + + floats[0] = static_cast(position.GetX()); + floats[1] = static_cast(position.GetY()); + floats[2] = static_cast(position.GetZ()); + floats[3] = rotation.GetX(); + floats[4] = rotation.GetY(); + floats[5] = rotation.GetZ(); + floats[6] = rotation.GetW(); + floats[7] = linearVelocity.GetX(); + floats[8] = linearVelocity.GetY(); + floats[9] = linearVelocity.GetZ(); + floats[10] = angularVelocity.GetX(); + floats[11] = angularVelocity.GetY(); + floats[12] = angularVelocity.GetZ(); + floats[13] = body.m_Mass; + floats[14] = bodyInterface.GetFriction(body.m_BodyId); + floats[15] = bodyInterface.GetRestitution(body.m_BodyId); + floats[16] = linearDamping; + floats[17] = angularDamping; + floats[18] = body.m_CenterOfMassOffsetY; + floats[19] = body.m_HalfExtentX; + floats[20] = body.m_HalfExtentY; + floats[21] = body.m_HalfExtentZ; + floats[22] = body.m_Radius; + floats[23] = body.m_HalfHeight; + + ints[0] = body.m_ShapeType; + ints[1] = body.m_BodyType; + ints[2] = bodyInterface.IsActive(body.m_BodyId) ? 0 : 1; + ints[3] = bodyInterface.IsSensor(body.m_BodyId) ? 1 : 0; + ints[4] = body.m_CollisionGroup; + ints[5] = body.m_CollisionMask; + ints[6] = bodyInterface.GetMotionQuality(body.m_BodyId) + == JPH::EMotionQuality::LinearCast + ? 1 + : 0; + ints[7] = body.m_HasBoxHalfExtents ? 1 : 0; + ints[8] = body.m_Axis; +} + +} // namespace ImpulseJolt diff --git a/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_snapshot.h b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_snapshot.h new file mode 100644 index 00000000..a509c87a --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_snapshot.h @@ -0,0 +1,9 @@ +#pragma once + +#include "internal/impulse_jolt_space.h" + +namespace ImpulseJolt { + +void WriteSnapshot(Space& targetSpace, const BodyState& body, float* floats, int* ints); + +} // namespace ImpulseJolt diff --git a/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_space.cpp b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_space.cpp new file mode 100644 index 00000000..096edc2b --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_space.cpp @@ -0,0 +1,174 @@ +#include "internal/impulse_jolt_registry.h" +#include "internal/impulse_jolt_space.h" + +#include + +#include +#include + +namespace ImpulseJolt { + +ImpulseContactListener::ImpulseContactListener(Space* owner) + : m_Owner(owner) { +} + +Space::Space() + : m_BroadPhaseLayerInterface(1), + m_ObjectVsBroadphaseLayerFilter(m_BroadPhaseLayerInterface), + m_ContactListener(this), + m_TempAllocator(10 * 1024 * 1024), + m_JobSystem(JPH::cMaxPhysicsJobs, + JPH::cMaxPhysicsBarriers, + std::max(1U, std::thread::hardware_concurrency())) { + m_BroadPhaseLayerInterface.ConfigureLayer( + JPH::BroadPhaseLayer(0), + JPH::ObjectLayerPairFilterMask::cMask, + 0); + m_PhysicsSystem.Init(MaxBodies, + 0, + MaxBodyPairs, + MaxContactConstraints, + m_BroadPhaseLayerInterface, + m_ObjectVsBroadphaseLayerFilter, + m_ObjectLayerFilter); + m_PhysicsSystem.SetContactListener(&m_ContactListener); + m_PhysicsSystem.SetGravity(JPH::Vec3(0.0F, -9.81F, 0.0F)); +} + +Space::~Space() { + for (auto& entry : m_Joints) { + JointState& joint = entry.second; + if (joint.m_Constraint != nullptr) { + m_PhysicsSystem.RemoveConstraint(joint.m_Constraint); + } + } + m_Joints.clear(); + JPH::BodyInterface& bodyInterface = m_PhysicsSystem.GetBodyInterface(); + for (auto& entry : m_Bodies) { + BodyState& body = entry.second; + if (!body.m_BodyId.IsInvalid()) { + if (bodyInterface.IsAdded(body.m_BodyId)) { + bodyInterface.RemoveBody(body.m_BodyId); + } + bodyInterface.DestroyBody(body.m_BodyId); + } + } +} + +void Space::ReplaceContactRecords(const JPH::Body& body1, + const JPH::Body& body2, + const JPH::ContactManifold& manifold) { + std::uint64_t bodyAHandle = NativeHandleForBodyId(*this, body1.GetID()); + std::uint64_t bodyBHandle = NativeHandleForBodyId(*this, body2.GetID()); + if (bodyAHandle == 0 || bodyBHandle == 0) { + return; + } + + JPH::SubShapeIDPair key(body1.GetID(), + manifold.mSubShapeID1, + body2.GetID(), + manifold.mSubShapeID2); + std::lock_guard lock(m_ContactMutex); + m_Contacts.erase(std::remove_if(m_Contacts.begin(), + m_Contacts.end(), + [&key](const ContactRecord& record) { + return record.m_Key == key; + }), + m_Contacts.end()); + + for (JPH::uint index = 0; index < manifold.mRelativeContactPointsOn1.size(); index++) { + JPH::RVec3 pointA = manifold.GetWorldSpaceContactPointOn1(index); + JPH::RVec3 pointB = manifold.GetWorldSpaceContactPointOn2(index); + ContactRecord record; + record.m_Key = key; + record.m_BodyAHandle = bodyAHandle; + record.m_BodyBHandle = bodyBHandle; + record.m_PointAX = static_cast(pointA.GetX()); + record.m_PointAY = static_cast(pointA.GetY()); + record.m_PointAZ = static_cast(pointA.GetZ()); + record.m_PointBX = static_cast(pointB.GetX()); + record.m_PointBY = static_cast(pointB.GetY()); + record.m_PointBZ = static_cast(pointB.GetZ()); + record.m_NormalBX = manifold.mWorldSpaceNormal.GetX(); + record.m_NormalBY = manifold.mWorldSpaceNormal.GetY(); + record.m_NormalBZ = manifold.mWorldSpaceNormal.GetZ(); + record.m_Distance = -manifold.mPenetrationDepth; + record.m_Impulse = 0.0F; + m_Contacts.push_back(record); + } +} + +void Space::EraseContactRecords(const JPH::SubShapeIDPair& key) { + std::lock_guard lock(m_ContactMutex); + m_Contacts.erase(std::remove_if(m_Contacts.begin(), + m_Contacts.end(), + [&key](const ContactRecord& record) { + return record.m_Key == key; + }), + m_Contacts.end()); +} + +void Space::EraseContactRecordsForBodyHandle(std::uint64_t bodyHandle) { + std::lock_guard lock(m_ContactMutex); + m_Contacts.erase(std::remove_if(m_Contacts.begin(), + m_Contacts.end(), + [bodyHandle](const ContactRecord& record) { + return record.m_BodyAHandle == bodyHandle + || record.m_BodyBHandle == bodyHandle; + }), + m_Contacts.end()); +} + +void Space::EraseConstraintsForBodyHandle(std::uint64_t bodyHandle) { + for (auto iterator = m_Joints.begin(); iterator != m_Joints.end();) { + JointState& joint = iterator->second; + if (joint.m_BodyAHandle != bodyHandle && joint.m_BodyBHandle != bodyHandle) { + ++iterator; + continue; + } + if (joint.m_Constraint != nullptr) { + m_PhysicsSystem.RemoveConstraint(joint.m_Constraint); + } + iterator = m_Joints.erase(iterator); + } +} + +bool Space::RemoveJointHandle(std::uint64_t jointHandle) { + auto iterator = m_Joints.find(jointHandle); + if (iterator == m_Joints.end()) { + return false; + } + if (iterator->second.m_Constraint != nullptr) { + m_PhysicsSystem.RemoveConstraint(iterator->second.m_Constraint); + } + m_Joints.erase(iterator); + return true; +} + +void ImpulseContactListener::OnContactAdded(const JPH::Body& body1, + const JPH::Body& body2, + const JPH::ContactManifold& manifold, + JPH::ContactSettings& settings) { + (void) settings; + if (m_Owner != nullptr) { + m_Owner->ReplaceContactRecords(body1, body2, manifold); + } +} + +void ImpulseContactListener::OnContactPersisted(const JPH::Body& body1, + const JPH::Body& body2, + const JPH::ContactManifold& manifold, + JPH::ContactSettings& settings) { + (void) settings; + if (m_Owner != nullptr) { + m_Owner->ReplaceContactRecords(body1, body2, manifold); + } +} + +void ImpulseContactListener::OnContactRemoved(const JPH::SubShapeIDPair& subShapePair) { + if (m_Owner != nullptr) { + m_Owner->EraseContactRecords(subShapePair); + } +} + +} // namespace ImpulseJolt diff --git a/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_space.h b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_space.h new file mode 100644 index 00000000..91ab8a6e --- /dev/null +++ b/impulse-backends/jolt/src/main/cpp/internal/impulse_jolt_space.h @@ -0,0 +1,122 @@ +#pragma once + +#include "abi/impulse_jolt_native.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace ImpulseJolt { + +struct Space; + +struct ContactRecord { + JPH::SubShapeIDPair m_Key; + std::uint64_t m_BodyAHandle = 0; + std::uint64_t m_BodyBHandle = 0; + float m_PointAX = 0.0F; + float m_PointAY = 0.0F; + float m_PointAZ = 0.0F; + float m_PointBX = 0.0F; + float m_PointBY = 0.0F; + float m_PointBZ = 0.0F; + float m_NormalBX = 0.0F; + float m_NormalBY = 0.0F; + float m_NormalBZ = 0.0F; + float m_Distance = 0.0F; + float m_Impulse = 0.0F; +}; + +class ImpulseContactListener final : public JPH::ContactListener { +public: + explicit ImpulseContactListener(Space* owner); + + void OnContactAdded(const JPH::Body& body1, + const JPH::Body& body2, + const JPH::ContactManifold& manifold, + JPH::ContactSettings& settings) override; + + void OnContactPersisted(const JPH::Body& body1, + const JPH::Body& body2, + const JPH::ContactManifold& manifold, + JPH::ContactSettings& settings) override; + + void OnContactRemoved(const JPH::SubShapeIDPair& subShapePair) override; + +private: + Space* m_Owner; +}; + +struct BodyState { + JPH::BodyID m_BodyId; + int m_ShapeType = 0; + int m_BodyType = 0; + bool m_Sensor = false; + float m_Mass = 0.0F; + float m_Friction = 0.0F; + float m_Restitution = 0.0F; + float m_LinearDamping = 0.0F; + float m_AngularDamping = 0.0F; + int m_CollisionGroup = 0; + int m_CollisionMask = 0; + bool m_ContinuousCollision = false; + float m_CenterOfMassOffsetY = 0.0F; + bool m_HasBoxHalfExtents = false; + float m_HalfExtentX = 0.0F; + float m_HalfExtentY = 0.0F; + float m_HalfExtentZ = 0.0F; + float m_Radius = 0.0F; + float m_HalfHeight = 0.0F; + int m_Axis = AxisY; +}; + +struct JointState { + JPH::Ref m_Constraint; + std::uint64_t m_BodyAHandle = 0; + std::uint64_t m_BodyBHandle = 0; + int m_JointType = 0; +}; + +struct Space { + JPH::BroadPhaseLayerInterfaceMask m_BroadPhaseLayerInterface; + JPH::ObjectVsBroadPhaseLayerFilterMask m_ObjectVsBroadphaseLayerFilter; + JPH::ObjectLayerPairFilterMask m_ObjectLayerFilter; + JPH::PhysicsSystem m_PhysicsSystem; + ImpulseContactListener m_ContactListener; + JPH::TempAllocatorImpl m_TempAllocator; + JPH::JobSystemThreadPool m_JobSystem; + std::unordered_map m_Bodies; + std::unordered_map m_BodyHandlesByJoltId; + std::unordered_map m_Joints; + std::mutex m_ContactMutex; + std::vector m_Contacts; + + Space(); + ~Space(); + + void ReplaceContactRecords(const JPH::Body& body1, + const JPH::Body& body2, + const JPH::ContactManifold& manifold); + + void EraseContactRecords(const JPH::SubShapeIDPair& key); + + void EraseContactRecordsForBodyHandle(std::uint64_t bodyHandle); + + void EraseConstraintsForBodyHandle(std::uint64_t bodyHandle); + + bool RemoveJointHandle(std::uint64_t jointHandle); +}; + +} // namespace ImpulseJolt diff --git a/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltBackend.java b/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltBackend.java new file mode 100644 index 00000000..d8745c11 --- /dev/null +++ b/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltBackend.java @@ -0,0 +1,38 @@ +package dev.hytalemodding.impulse.jolt; + +import dev.hytalemodding.impulse.api.BackendId; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.annotation.Nonnull; + +public final class JoltBackend { + + public static final BackendId ID = new BackendId("impulse:jolt"); + private static final Logger LOGGER = Logger.getLogger("Impulse"); + + private volatile boolean initialized; + private JoltNativeLibrary nativeLibrary; + + @Nonnull + public BackendId getId() { + return ID; + } + + public synchronized void init() { + if (initialized) { + return; + } + + nativeLibrary = JoltNative.open(); + initialized = true; + LOGGER.log(Level.INFO, "Jolt backend initialized"); + } + + @Nonnull + synchronized JoltNativeLibrary nativeLibrary() { + if (!initialized) { + init(); + } + return nativeLibrary; + } +} diff --git a/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltBackendRuntime.java b/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltBackendRuntime.java new file mode 100644 index 00000000..1b6808db --- /dev/null +++ b/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltBackendRuntime.java @@ -0,0 +1,829 @@ +package dev.hytalemodding.impulse.jolt; + +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.capability.PhysicsActivationTuning; +import dev.hytalemodding.impulse.api.capability.PhysicsCapabilityId; +import dev.hytalemodding.impulse.api.capability.PhysicsSolverTuning; +import dev.hytalemodding.impulse.api.runtime.BackendBodyIdSource; +import dev.hytalemodding.impulse.api.runtime.BackendBodySnapshotSink; +import dev.hytalemodding.impulse.api.runtime.BackendContactSink; +import dev.hytalemodding.impulse.api.runtime.BackendExtensionSettingsSource; +import dev.hytalemodding.impulse.api.runtime.BackendJointType; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.runtime.BackendRayHitSink; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeStatsSink; +import dev.hytalemodding.impulse.api.runtime.BackendStepPhaseStatsSink; +import dev.hytalemodding.impulse.api.runtime.BackendVec3Sink; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +final class JoltBackendRuntime implements PhysicsBackendRuntime { + + private static final String UNSUPPORTED_MESSAGE = + "Jolt runtime operation is not implemented yet"; + + private final JoltBackend backend; + @Nullable + private final JoltNativeLibrary fixedNativeLibrary; + private final Map spaces = new HashMap<>(); + private final float[] gravityScratch = new float[3]; + private final long[] closestRayBodyHandleScratch = new long[1]; + private final float[] closestRayHitScratch = new float[JoltNativeLibrary.RAY_HIT_FLOAT_COUNT]; + private final JoltBodySnapshot bodySnapshotScratch = new JoltBodySnapshot(); + + JoltBackendRuntime(@Nonnull JoltBackend backend) { + this(backend, null); + } + + JoltBackendRuntime(@Nonnull JoltBackend backend, @Nullable JoltNativeLibrary nativeLibrary) { + this.backend = Objects.requireNonNull(backend, "backend"); + this.fixedNativeLibrary = nativeLibrary; + } + + @Override + public int createSpace(@Nonnull SpaceId requestedId) { + Objects.requireNonNull(requestedId, "requestedId"); + int spaceId = requestedId.value(); + if (spaces.containsKey(spaceId)) { + throw new IllegalStateException("Jolt space already exists: " + spaceId); + } + long handle = nativeLibrary().createSpace(); + if (handle == 0L) { + throw new IllegalStateException("Jolt native library returned a null space handle"); + } + spaces.put(spaceId, new RuntimeSpace(handle)); + return spaceId; + } + + @Override + public void destroySpace(int spaceId) { + RuntimeSpace space = spaces.remove(spaceId); + if (space != null) { + nativeLibrary().destroySpace(space.handle); + } + } + + @Override + public void close() { + RuntimeException failure = null; + for (Integer spaceId : new ArrayList<>(spaces.keySet())) { + try { + destroySpace(spaceId); + } catch (RuntimeException exception) { + if (failure == null) { + failure = exception; + } else { + failure.addSuppressed(exception); + } + } + } + if (failure != null) { + throw failure; + } + } + + @Override + public void step(int spaceId, float dt) { + long handle = requireSpaceHandle(spaceId); + if (dt <= 0.0f) { + return; + } + nativeLibrary().step(handle, dt); + } + + @Override + public void setGravity(int spaceId, float x, float y, float z) { + nativeLibrary().setGravity(requireSpaceHandle(spaceId), x, y, z); + } + + @Override + public void getGravity(int spaceId, @Nonnull BackendVec3Sink sink) { + Objects.requireNonNull(sink, "sink"); + nativeLibrary().getGravity(requireSpaceHandle(spaceId), gravityScratch); + sink.accept(gravityScratch[0], gravityScratch[1], gravityScratch[2]); + } + + @Override + public long createBody(int spaceId, + int shapeTypeCode, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode, + float groundY, + float mass, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW) { + RuntimeSpace space = requireSpace(spaceId); + validateBodyShapeCode(shapeTypeCode); + BackendRuntimeCodes.axis(axisCode); + BackendRuntimeCodes.bodyType(bodyTypeCode); + long bodyHandle = nativeLibrary().createBody(space.handle, + shapeTypeCode, + halfExtentX, + halfExtentY, + halfExtentZ, + radius, + halfHeight, + axisCode, + groundY, + mass, + bodyTypeCode, + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW); + if (bodyHandle == 0L) { + throw new IllegalStateException("Jolt native library returned a null body handle"); + } + long bodyId = space.nextBodyId++; + space.bodyHandles.put(bodyId, bodyHandle); + space.bodyIdsByHandle.put(bodyHandle, bodyId); + return bodyId; + } + + @Override + public boolean supportsVoxelTerrain(int spaceId) { + requireSpaceHandle(spaceId); + return false; + } + + @Override + public long createVoxelTerrain(int spaceId, + float voxelSizeX, + float voxelSizeY, + float voxelSizeZ, + @Nonnull int[] voxelCoordinates, + float positionX, + float positionY, + float positionZ, + float friction, + float restitution, + int collisionGroup, + int collisionMask) { + Objects.requireNonNull(voxelCoordinates, "voxelCoordinates"); + requireSpaceHandle(spaceId); + throw unsupported(); + } + + @Override + public void combineVoxelTerrains(int spaceId, + long bodyAId, + long bodyBId, + int shiftX, + int shiftY, + int shiftZ) { + requireSpaceHandle(spaceId); + throw unsupported(); + } + + @Override + public void removeBody(int spaceId, long bodyId) { + RuntimeSpace space = requireSpace(spaceId); + Long bodyHandle = space.bodyHandles.remove(bodyId); + if (bodyHandle != null) { + space.bodyIdsByHandle.remove(bodyHandle); + nativeLibrary().removeBody(space.handle, bodyHandle); + removeAttachedJointState(space, bodyId); + } + } + + @Override + public int bodyCount(int spaceId) { + return nativeLibrary().bodyCount(requireSpaceHandle(spaceId)); + } + + @Override + public boolean containsBody(int spaceId, long bodyId) { + RuntimeSpace space = requireSpace(spaceId); + Long bodyHandle = space.bodyHandles.get(bodyId); + return bodyHandle != null && nativeLibrary().containsBody(space.handle, bodyHandle); + } + + @Override + public boolean bodySnapshot(int spaceId, long bodyId, @Nonnull BackendBodySnapshotSink sink) { + Objects.requireNonNull(sink, "sink"); + RuntimeSpace space = requireSpace(spaceId); + Long bodyHandle = space.bodyHandles.get(bodyId); + if (bodyHandle == null) { + return false; + } + bodySnapshotScratch.clear(); + if (!nativeLibrary().bodySnapshot(space.handle, bodyHandle, bodySnapshotScratch)) { + return false; + } + bodySnapshotScratch.emit(bodyId, sink); + return true; + } + + @Override + public void snapshotBodies(int spaceId, + @Nonnull BackendBodyIdSource bodyIds, + @Nonnull BackendBodySnapshotSink sink) { + Objects.requireNonNull(bodyIds, "bodyIds"); + Objects.requireNonNull(sink, "sink"); + RuntimeSpace space = requireSpace(spaceId); + bodyIds.forEachBodyId(bodyId -> emitBodySnapshotIfPresent(space, bodyId, sink)); + } + + @Override + public void setBodyTransform(int spaceId, + long bodyId, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW) { + RuntimeSpace space = requireSpace(spaceId); + nativeLibrary().setBodyTransform(space.handle, + requireBodyHandle(space, bodyId), + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW); + } + + @Override + public void setBodyPosition(int spaceId, long bodyId, float x, float y, float z) { + RuntimeSpace space = requireSpace(spaceId); + nativeLibrary().setBodyPosition(space.handle, requireBodyHandle(space, bodyId), x, y, z); + } + + @Override + public void setBodyVelocity(int spaceId, + long bodyId, + float linearX, + float linearY, + float linearZ, + float angularX, + float angularY, + float angularZ) { + RuntimeSpace space = requireSpace(spaceId); + nativeLibrary().setBodyVelocity(space.handle, + requireBodyHandle(space, bodyId), + linearX, + linearY, + linearZ, + angularX, + angularY, + angularZ); + } + + @Override + public void setBodyType(int spaceId, long bodyId, int bodyTypeCode) { + RuntimeSpace space = requireSpace(spaceId); + BackendRuntimeCodes.bodyType(bodyTypeCode); + nativeLibrary().setBodyType(space.handle, requireBodyHandle(space, bodyId), bodyTypeCode); + } + + @Override + public void setBodyDamping(int spaceId, long bodyId, float linearDamping, float angularDamping) { + RuntimeSpace space = requireSpace(spaceId); + nativeLibrary().setBodyDamping(space.handle, + requireBodyHandle(space, bodyId), + linearDamping, + angularDamping); + } + + @Override + public void setBodyFriction(int spaceId, long bodyId, float friction) { + RuntimeSpace space = requireSpace(spaceId); + nativeLibrary().setBodyFriction(space.handle, requireBodyHandle(space, bodyId), friction); + } + + @Override + public void setBodyRestitution(int spaceId, long bodyId, float restitution) { + RuntimeSpace space = requireSpace(spaceId); + nativeLibrary().setBodyRestitution(space.handle, + requireBodyHandle(space, bodyId), + restitution); + } + + @Override + public void setBodyCollisionFilter(int spaceId, long bodyId, int group, int mask) { + RuntimeSpace space = requireSpace(spaceId); + nativeLibrary().setBodyCollisionFilter(space.handle, + requireBodyHandle(space, bodyId), + group, + mask); + } + + @Override + public void setBodySensor(int spaceId, long bodyId, boolean sensor) { + RuntimeSpace space = requireSpace(spaceId); + nativeLibrary().setBodySensor(space.handle, requireBodyHandle(space, bodyId), sensor); + } + + @Override + public void setBodyContinuousCollision(int spaceId, long bodyId, boolean enabled) { + RuntimeSpace space = requireSpace(spaceId); + nativeLibrary().setBodyContinuousCollision(space.handle, + requireBodyHandle(space, bodyId), + enabled); + } + + @Override + public boolean isBodyContinuousCollisionEnabled(int spaceId, long bodyId) { + RuntimeSpace space = requireSpace(spaceId); + return nativeLibrary().isBodyContinuousCollisionEnabled(space.handle, + requireBodyHandle(space, bodyId)); + } + + @Override + public void activateBody(int spaceId, long bodyId) { + RuntimeSpace space = requireSpace(spaceId); + nativeLibrary().activateBody(space.handle, requireBodyHandle(space, bodyId)); + } + + @Override + public void sleepBody(int spaceId, long bodyId) { + RuntimeSpace space = requireSpace(spaceId); + nativeLibrary().sleepBody(space.handle, requireBodyHandle(space, bodyId)); + } + + @Override + public void applyBodyImpulse(int spaceId, + long bodyId, + float x, + float y, + float z, + boolean hasOffset, + float offsetX, + float offsetY, + float offsetZ, + boolean torque) { + RuntimeSpace space = requireSpace(spaceId); + nativeLibrary().applyBodyImpulse(space.handle, + requireBodyHandle(space, bodyId), + x, + y, + z, + hasOffset, + offsetX, + offsetY, + offsetZ, + torque); + } + + @Override + public void applyBodyForce(int spaceId, + long bodyId, + float x, + float y, + float z, + boolean hasOffset, + float offsetX, + float offsetY, + float offsetZ, + boolean torque) { + RuntimeSpace space = requireSpace(spaceId); + nativeLibrary().applyBodyForce(space.handle, + requireBodyHandle(space, bodyId), + x, + y, + z, + hasOffset, + offsetX, + offsetY, + offsetZ, + torque); + } + + @Override + public long createJoint(int spaceId, + int jointTypeCode, + long bodyAId, + long bodyBId, + float anchorAX, + float anchorAY, + float anchorAZ, + float anchorBX, + float anchorBY, + float anchorBZ, + float axisX, + float axisY, + float axisZ, + float restLength, + float stiffness, + float damping, + float lowerLimit, + float upperLimit, + boolean motorEnabled, + float motorTargetVelocity, + float motorMaxForce) { + RuntimeSpace space = requireSpace(spaceId); + BackendJointType type = BackendRuntimeCodes.jointType(jointTypeCode); + if (bodyAId == bodyBId) { + throw new IllegalArgumentException("Jolt joint endpoints must reference distinct bodies"); + } + long bodyAHandle = requireBodyHandle(space, bodyAId); + long bodyBHandle = requireBodyHandle(space, bodyBId); + NormalizedAxis axis = normalizeAxis(axisX, axisY, axisZ); + long jointHandle = nativeLibrary().createJoint(space.handle, + BackendRuntimeCodes.jointTypeCode(type), + bodyAHandle, + bodyBHandle, + anchorAX, + anchorAY, + anchorAZ, + anchorBX, + anchorBY, + anchorBZ, + axis.x, + axis.y, + axis.z, + restLength, + stiffness, + damping, + lowerLimit, + upperLimit, + motorEnabled, + motorTargetVelocity, + motorMaxForce); + if (jointHandle == 0L) { + throw new IllegalStateException("Jolt native library returned a null joint handle"); + } + long jointId = space.nextJointId++; + space.jointsById.put(jointId, new JointState(jointHandle, + jointTypeCode, + bodyAId, + bodyBId)); + return jointId; + } + + @Override + public void removeJoint(int spaceId, long jointId) { + RuntimeSpace space = requireSpace(spaceId); + JointState joint = space.jointsById.get(jointId); + if (joint != null) { + nativeLibrary().removeJoint(space.handle, joint.nativeJointHandle); + space.jointsById.remove(jointId); + } + } + + @Override + public int jointCount(int spaceId) { + return requireSpace(spaceId).jointsById.size(); + } + + @Override + public int jointType(int spaceId, long jointId) { + return requireJoint(requireSpace(spaceId), jointId).jointTypeCode; + } + + @Override + public long jointBodyA(int spaceId, long jointId) { + return requireJoint(requireSpace(spaceId), jointId).bodyAId; + } + + @Override + public long jointBodyB(int spaceId, long jointId) { + return requireJoint(requireSpace(spaceId), jointId).bodyBId; + } + + @Override + public boolean raycastClosest(int spaceId, + float fromX, + float fromY, + float fromZ, + float toX, + float toY, + float toZ, + @Nonnull BackendRayHitSink sink) { + Objects.requireNonNull(sink, "sink"); + RuntimeSpace space = requireSpace(spaceId); + int hitCount = nativeLibrary().raycastClosest(space.handle, + fromX, + fromY, + fromZ, + toX, + toY, + toZ, + closestRayBodyHandleScratch, + closestRayHitScratch); + return hitCount > 0 && emitRayHit(space, + closestRayBodyHandleScratch[0], + closestRayHitScratch, + 0, + sink); + } + + @Override + public int raycastAll(int spaceId, + float fromX, + float fromY, + float fromZ, + float toX, + float toY, + float toZ, + @Nonnull BackendRayHitSink sink) { + Objects.requireNonNull(sink, "sink"); + RuntimeSpace space = requireSpace(spaceId); + int maxHits = nativeLibrary().bodyCount(space.handle); + if (maxHits <= 0) { + return 0; + } + long[] bodyHandles = new long[maxHits]; + float[] hits = new float[maxHits * JoltNativeLibrary.RAY_HIT_FLOAT_COUNT]; + int nativeHits = nativeLibrary().raycastAll(space.handle, + fromX, + fromY, + fromZ, + toX, + toY, + toZ, + maxHits, + bodyHandles, + hits); + int emitted = 0; + int boundedHits = Math.clamp(nativeHits, 0, maxHits); + for (int index = 0; index < boundedHits; index++) { + if (emitRayHit(space, + bodyHandles[index], + hits, + index * JoltNativeLibrary.RAY_HIT_FLOAT_COUNT, + sink)) { + emitted++; + } + } + return emitted; + } + + @Override + public int contacts(int spaceId, @Nonnull BackendContactSink sink) { + Objects.requireNonNull(sink, "sink"); + RuntimeSpace space = requireSpace(spaceId); + int maxContacts = nativeLibrary().contactCount(space.handle); + return emitContacts(space, maxContacts, sink); + } + + @Override + public int contacts(int spaceId, int maxContacts, @Nonnull BackendContactSink sink) { + Objects.requireNonNull(sink, "sink"); + RuntimeSpace space = requireSpace(spaceId); + return emitContacts(space, maxContacts, sink); + } + + @Override + public int contactCount(int spaceId) { + return nativeLibrary().contactCount(requireSpaceHandle(spaceId)); + } + + @Override + public void runtimeStats(int spaceId, @Nonnull BackendRuntimeStatsSink sink) { + Objects.requireNonNull(sink, "sink"); + long handle = requireSpaceHandle(spaceId); + int bodyCount = nativeLibrary().bodyCount(handle); + int jointCount = nativeLibrary().jointCount(handle); + sink.accept(bodyCount, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + jointCount, + true); + } + + @Override + public void resetStepPhaseStats(int spaceId) { + requireSpaceHandle(spaceId); + } + + @Override + public void stepPhaseStats(int spaceId, @Nonnull BackendStepPhaseStatsSink sink) { + Objects.requireNonNull(sink, "sink"); + requireSpaceHandle(spaceId); + sink.accept(0L, 0L, 0L, 0L, 0L, 0L, false); + } + + @Override + public boolean supportsContinuousCollision(int spaceId) { + requireSpaceHandle(spaceId); + return true; + } + + @Override + public boolean supportsSolverTuning(int spaceId) { + requireSpaceHandle(spaceId); + return false; + } + + @Override + public boolean supportsActivationTuning(int spaceId) { + requireSpaceHandle(spaceId); + return false; + } + + @Override + public void applySolverTuning(int spaceId, @Nonnull PhysicsSolverTuning tuning) { + Objects.requireNonNull(tuning, "tuning"); + requireSpaceHandle(spaceId); + } + + @Override + public void applyActivationTuning(int spaceId, @Nonnull PhysicsActivationTuning tuning) { + Objects.requireNonNull(tuning, "tuning"); + requireSpaceHandle(spaceId); + } + + @Override + public void applyExtensionSettings(int spaceId, + @Nonnull PhysicsCapabilityId capabilityId, + @Nonnull BackendExtensionSettingsSource settings) { + Objects.requireNonNull(capabilityId, "capabilityId"); + Objects.requireNonNull(settings, "settings"); + requireSpaceHandle(spaceId); + } + + @Nonnull + JoltBackend backend() { + return backend; + } + + private long requireSpaceHandle(int spaceId) { + return requireSpace(spaceId).handle; + } + + @Nonnull + private RuntimeSpace requireSpace(int spaceId) { + RuntimeSpace space = spaces.get(spaceId); + if (space == null) { + throw new IllegalArgumentException("Unknown Jolt space id: " + spaceId); + } + return space; + } + + private long requireBodyHandle(@Nonnull RuntimeSpace space, long bodyId) { + Long bodyHandle = space.bodyHandles.get(bodyId); + if (bodyHandle == null) { + throw new IllegalArgumentException("Unknown Jolt body id: " + bodyId); + } + return bodyHandle; + } + + @Nonnull + private static JointState requireJoint(@Nonnull RuntimeSpace space, long jointId) { + JointState joint = space.jointsById.get(jointId); + if (joint == null) { + throw new IllegalArgumentException("Unknown Jolt joint id: " + jointId); + } + return joint; + } + + private static void removeAttachedJointState(@Nonnull RuntimeSpace space, long bodyId) { + space.jointsById.values().removeIf(joint -> joint.bodyAId == bodyId + || joint.bodyBId == bodyId); + } + + private void emitBodySnapshotIfPresent(@Nonnull RuntimeSpace space, + long bodyId, + @Nonnull BackendBodySnapshotSink sink) { + Long bodyHandle = space.bodyHandles.get(bodyId); + if (bodyHandle == null) { + return; + } + bodySnapshotScratch.clear(); + if (nativeLibrary().bodySnapshot(space.handle, bodyHandle, bodySnapshotScratch)) { + bodySnapshotScratch.emit(bodyId, sink); + } + } + + private boolean emitRayHit(@Nonnull RuntimeSpace space, + long bodyHandle, + @Nonnull float[] hit, + int offset, + @Nonnull BackendRayHitSink sink) { + Long bodyId = space.bodyIdsByHandle.get(bodyHandle); + if (bodyId == null) { + return false; + } + sink.accept(bodyId, + hit[offset], + hit[offset + 1], + hit[offset + 2], + hit[offset + 3], + hit[offset + 4], + hit[offset + 5], + hit[offset + 6], + hit[offset + 7]); + return true; + } + + private int emitContacts(@Nonnull RuntimeSpace space, + int maxContacts, + @Nonnull BackendContactSink sink) { + if (maxContacts <= 0) { + return 0; + } + long[] bodyHandles = new long[maxContacts * JoltNativeLibrary.CONTACT_BODY_HANDLE_COUNT]; + float[] contacts = new float[maxContacts * JoltNativeLibrary.CONTACT_FLOAT_COUNT]; + int nativeContacts = nativeLibrary().contacts(space.handle, + maxContacts, + bodyHandles, + contacts); + int emitted = 0; + int boundedContacts = Math.clamp(nativeContacts, 0, maxContacts); + for (int index = 0; index < boundedContacts; index++) { + int bodyOffset = index * JoltNativeLibrary.CONTACT_BODY_HANDLE_COUNT; + Long bodyAId = space.bodyIdsByHandle.get(bodyHandles[bodyOffset]); + Long bodyBId = space.bodyIdsByHandle.get(bodyHandles[bodyOffset + 1]); + if (bodyAId == null || bodyBId == null) { + continue; + } + int contactOffset = index * JoltNativeLibrary.CONTACT_FLOAT_COUNT; + sink.accept(bodyAId, + bodyBId, + contacts[contactOffset], + contacts[contactOffset + 1], + contacts[contactOffset + 2], + contacts[contactOffset + 3], + contacts[contactOffset + 4], + contacts[contactOffset + 5], + contacts[contactOffset + 6], + contacts[contactOffset + 7], + contacts[contactOffset + 8], + contacts[contactOffset + 9], + contacts[contactOffset + 10]); + emitted++; + } + return emitted; + } + + private static void validateBodyShapeCode(int shapeTypeCode) { + switch (BackendRuntimeCodes.shapeType(shapeTypeCode)) { + case BOX, SPHERE, CAPSULE, CYLINDER, CONE, PLANE -> { + } + case VOXELS, UNKNOWN -> throw new IllegalArgumentException( + "Unsupported Jolt body shape code: " + shapeTypeCode); + } + } + + @Nonnull + private static NormalizedAxis normalizeAxis(float axisX, float axisY, float axisZ) { + float lengthSquared = axisX * axisX + axisY * axisY + axisZ * axisZ; + if (lengthSquared == 0.0f) { + return new NormalizedAxis(0.0f, 1.0f, 0.0f); + } + float inverseLength = (float) (1.0 / Math.sqrt(lengthSquared)); + return new NormalizedAxis(axisX * inverseLength, + axisY * inverseLength, + axisZ * inverseLength); + } + + @Nonnull + private JoltNativeLibrary nativeLibrary() { + return fixedNativeLibrary != null ? fixedNativeLibrary : backend.nativeLibrary(); + } + + private static UnsupportedOperationException unsupported() { + return new UnsupportedOperationException(UNSUPPORTED_MESSAGE); + } + + private record NormalizedAxis(float x, float y, float z) { + } + + private record JointState(long nativeJointHandle, + int jointTypeCode, + long bodyAId, + long bodyBId) { + } + + private static final class RuntimeSpace { + + private final long handle; + private final Map bodyHandles = new HashMap<>(); + private final Map bodyIdsByHandle = new HashMap<>(); + private final Map jointsById = new HashMap<>(); + private long nextBodyId = 1L; + private long nextJointId = 1L; + + private RuntimeSpace(long handle) { + this.handle = handle; + } + } +} diff --git a/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltBackendRuntimeProvider.java b/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltBackendRuntimeProvider.java new file mode 100644 index 00000000..56c53677 --- /dev/null +++ b/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltBackendRuntimeProvider.java @@ -0,0 +1,31 @@ +package dev.hytalemodding.impulse.jolt; + +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; +import javax.annotation.Nonnull; + +/** + * Runtime-provider service entry point for Jolt. + */ +public final class JoltBackendRuntimeProvider implements PhysicsBackendRuntimeProvider { + + private final JoltBackend backend = new JoltBackend(); + + @Nonnull + @Override + public BackendId getId() { + return JoltBackend.ID; + } + + @Override + public void init() { + backend.init(); + } + + @Nonnull + @Override + public PhysicsBackendRuntime createRuntime() { + return new JoltBackendRuntime(backend); + } +} diff --git a/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltBodySnapshot.java b/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltBodySnapshot.java new file mode 100644 index 00000000..eb7699d3 --- /dev/null +++ b/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltBodySnapshot.java @@ -0,0 +1,153 @@ +package dev.hytalemodding.impulse.jolt; + +import dev.hytalemodding.impulse.api.runtime.BackendBodySnapshotSink; +import javax.annotation.Nonnull; + +final class JoltBodySnapshot { + + static final int FLOAT_FIELD_COUNT = 24; + static final int INT_FIELD_COUNT = 9; + + int shapeTypeCode; + int bodyTypeCode; + float positionX; + float positionY; + float positionZ; + float rotationX; + float rotationY; + float rotationZ; + float rotationW; + float linearVelocityX; + float linearVelocityY; + float linearVelocityZ; + float angularVelocityX; + float angularVelocityY; + float angularVelocityZ; + boolean sleeping; + boolean sensor; + float mass; + float friction; + float restitution; + float linearDamping; + float angularDamping; + int collisionGroup; + int collisionMask; + boolean continuousCollisionEnabled; + float centerOfMassOffsetY; + boolean hasBoxHalfExtents; + float halfExtentX; + float halfExtentY; + float halfExtentZ; + float radius; + float halfHeight; + int axisCode; + + void clear() { + shapeTypeCode = 0; + bodyTypeCode = 0; + positionX = 0.0f; + positionY = 0.0f; + positionZ = 0.0f; + rotationX = 0.0f; + rotationY = 0.0f; + rotationZ = 0.0f; + rotationW = 0.0f; + linearVelocityX = 0.0f; + linearVelocityY = 0.0f; + linearVelocityZ = 0.0f; + angularVelocityX = 0.0f; + angularVelocityY = 0.0f; + angularVelocityZ = 0.0f; + sleeping = false; + sensor = false; + mass = 0.0f; + friction = 0.0f; + restitution = 0.0f; + linearDamping = 0.0f; + angularDamping = 0.0f; + collisionGroup = 0; + collisionMask = 0; + continuousCollisionEnabled = false; + centerOfMassOffsetY = 0.0f; + hasBoxHalfExtents = false; + halfExtentX = 0.0f; + halfExtentY = 0.0f; + halfExtentZ = 0.0f; + radius = 0.0f; + halfHeight = 0.0f; + axisCode = 0; + } + + void copyFrom(@Nonnull JoltBodySnapshot other) { + shapeTypeCode = other.shapeTypeCode; + bodyTypeCode = other.bodyTypeCode; + positionX = other.positionX; + positionY = other.positionY; + positionZ = other.positionZ; + rotationX = other.rotationX; + rotationY = other.rotationY; + rotationZ = other.rotationZ; + rotationW = other.rotationW; + linearVelocityX = other.linearVelocityX; + linearVelocityY = other.linearVelocityY; + linearVelocityZ = other.linearVelocityZ; + angularVelocityX = other.angularVelocityX; + angularVelocityY = other.angularVelocityY; + angularVelocityZ = other.angularVelocityZ; + sleeping = other.sleeping; + sensor = other.sensor; + mass = other.mass; + friction = other.friction; + restitution = other.restitution; + linearDamping = other.linearDamping; + angularDamping = other.angularDamping; + collisionGroup = other.collisionGroup; + collisionMask = other.collisionMask; + continuousCollisionEnabled = other.continuousCollisionEnabled; + centerOfMassOffsetY = other.centerOfMassOffsetY; + hasBoxHalfExtents = other.hasBoxHalfExtents; + halfExtentX = other.halfExtentX; + halfExtentY = other.halfExtentY; + halfExtentZ = other.halfExtentZ; + radius = other.radius; + halfHeight = other.halfHeight; + axisCode = other.axisCode; + } + + void emit(long bodyId, @Nonnull BackendBodySnapshotSink sink) { + sink.accept(bodyId, + shapeTypeCode, + bodyTypeCode, + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW, + linearVelocityX, + linearVelocityY, + linearVelocityZ, + angularVelocityX, + angularVelocityY, + angularVelocityZ, + sleeping, + sensor, + mass, + friction, + restitution, + linearDamping, + angularDamping, + collisionGroup, + collisionMask, + continuousCollisionEnabled, + centerOfMassOffsetY, + hasBoxHalfExtents, + halfExtentX, + halfExtentY, + halfExtentZ, + radius, + halfHeight, + axisCode); + } +} diff --git a/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltNative.java b/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltNative.java new file mode 100644 index 00000000..012e4a50 --- /dev/null +++ b/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltNative.java @@ -0,0 +1,31 @@ +package dev.hytalemodding.impulse.jolt; + +import dev.hytalemodding.impulse.internal.nativelib.NativeLibraryLoader; +import dev.hytalemodding.impulse.internal.nativelib.NativeLibraryResource; +import java.lang.foreign.SymbolLookup; + +final class JoltNative { + + private static final String LIBRARY_NAME = "impulse_jolt"; + private static JoltNativeLibrary library; + + private JoltNative() { + } + + static synchronized JoltNativeLibrary open() { + if (library != null) { + return library; + } + + try { + NativeLibraryLoader.load(JoltNative.class, + "jolt", + NativeLibraryResource.forCurrentPlatform(LIBRARY_NAME)); + } catch (IllegalArgumentException | IllegalStateException exception) { + throw new IllegalStateException("Failed to load the Jolt native library", exception); + } + + library = PanamaJoltNativeLibrary.open(SymbolLookup.loaderLookup()); + return library; + } +} diff --git a/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltNativeLibrary.java b/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltNativeLibrary.java new file mode 100644 index 00000000..a2191ac9 --- /dev/null +++ b/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/JoltNativeLibrary.java @@ -0,0 +1,224 @@ +package dev.hytalemodding.impulse.jolt; + +interface JoltNativeLibrary { + + int RAY_HIT_FLOAT_COUNT = 8; + int CONTACT_BODY_HANDLE_COUNT = 2; + int CONTACT_FLOAT_COUNT = 11; + + long createSpace(); + + void destroySpace(long spaceHandle); + + void step(long spaceHandle, float dt); + + void setGravity(long spaceHandle, float x, float y, float z); + + void getGravity(long spaceHandle, float[] out); + + default long createBody(long spaceHandle, + int shapeTypeCode, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode, + float groundY, + float mass, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW) { + throw unsupportedBodyAbi(); + } + + default void removeBody(long spaceHandle, long bodyHandle) { + throw unsupportedBodyAbi(); + } + + default boolean containsBody(long spaceHandle, long bodyHandle) { + throw unsupportedBodyAbi(); + } + + default boolean bodySnapshot(long spaceHandle, long bodyHandle, JoltBodySnapshot out) { + throw unsupportedBodyAbi(); + } + + default void setBodyTransform(long spaceHandle, + long bodyHandle, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW) { + throw unsupportedBodyAbi(); + } + + default void setBodyPosition(long spaceHandle, long bodyHandle, float x, float y, float z) { + throw unsupportedBodyAbi(); + } + + default void setBodyVelocity(long spaceHandle, + long bodyHandle, + float linearX, + float linearY, + float linearZ, + float angularX, + float angularY, + float angularZ) { + throw unsupportedBodyAbi(); + } + + default void setBodyType(long spaceHandle, long bodyHandle, int bodyTypeCode) { + throw unsupportedBodyAbi(); + } + + default void setBodyDamping(long spaceHandle, long bodyHandle, float linearDamping, float angularDamping) { + throw unsupportedBodyAbi(); + } + + default void setBodyFriction(long spaceHandle, long bodyHandle, float friction) { + throw unsupportedBodyAbi(); + } + + default void setBodyRestitution(long spaceHandle, long bodyHandle, float restitution) { + throw unsupportedBodyAbi(); + } + + default void setBodyCollisionFilter(long spaceHandle, long bodyHandle, int group, int mask) { + throw unsupportedBodyAbi(); + } + + default void setBodySensor(long spaceHandle, long bodyHandle, boolean sensor) { + throw unsupportedBodyAbi(); + } + + default void setBodyContinuousCollision(long spaceHandle, long bodyHandle, boolean enabled) { + throw unsupportedBodyAbi(); + } + + default boolean isBodyContinuousCollisionEnabled(long spaceHandle, long bodyHandle) { + throw unsupportedBodyAbi(); + } + + default void activateBody(long spaceHandle, long bodyHandle) { + throw unsupportedBodyAbi(); + } + + default void sleepBody(long spaceHandle, long bodyHandle) { + throw unsupportedBodyAbi(); + } + + default void applyBodyImpulse(long spaceHandle, + long bodyHandle, + float x, + float y, + float z, + boolean hasOffset, + float offsetX, + float offsetY, + float offsetZ, + boolean torque) { + throw unsupportedBodyAbi(); + } + + default void applyBodyForce(long spaceHandle, + long bodyHandle, + float x, + float y, + float z, + boolean hasOffset, + float offsetX, + float offsetY, + float offsetZ, + boolean torque) { + throw unsupportedBodyAbi(); + } + + default long createJoint(long spaceHandle, + int jointTypeCode, + long bodyAHandle, + long bodyBHandle, + float anchorAX, + float anchorAY, + float anchorAZ, + float anchorBX, + float anchorBY, + float anchorBZ, + float axisX, + float axisY, + float axisZ, + float restLength, + float stiffness, + float damping, + float lowerLimit, + float upperLimit, + boolean motorEnabled, + float motorTargetVelocity, + float motorMaxForce) { + throw unsupportedJointAbi(); + } + + default void removeJoint(long spaceHandle, long jointHandle) { + throw unsupportedJointAbi(); + } + + default int raycastClosest(long spaceHandle, + float fromX, + float fromY, + float fromZ, + float toX, + float toY, + float toZ, + long[] bodyHandleOut, + float[] hitOut) { + throw unsupportedQueryAbi(); + } + + default int raycastAll(long spaceHandle, + float fromX, + float fromY, + float fromZ, + float toX, + float toY, + float toZ, + int maxHits, + long[] bodyHandles, + float[] hits) { + throw unsupportedQueryAbi(); + } + + default int contacts(long spaceHandle, + int maxContacts, + long[] bodyHandles, + float[] contacts) { + throw unsupportedQueryAbi(); + } + + default int contactCount(long spaceHandle) { + throw unsupportedQueryAbi(); + } + + int bodyCount(long spaceHandle); + + int jointCount(long spaceHandle); + + private static UnsupportedOperationException unsupportedBodyAbi() { + return new UnsupportedOperationException("Jolt native body ABI is not implemented"); + } + + private static UnsupportedOperationException unsupportedQueryAbi() { + return new UnsupportedOperationException("Jolt native query ABI is not implemented"); + } + + private static UnsupportedOperationException unsupportedJointAbi() { + return new UnsupportedOperationException("Jolt native joint ABI is not implemented"); + } +} diff --git a/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/PanamaJoltNativeLibrary.java b/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/PanamaJoltNativeLibrary.java new file mode 100644 index 00000000..0ae9ad3e --- /dev/null +++ b/impulse-backends/jolt/src/main/java/dev/hytalemodding/impulse/jolt/PanamaJoltNativeLibrary.java @@ -0,0 +1,909 @@ +package dev.hytalemodding.impulse.jolt; + +import static java.lang.foreign.ValueLayout.ADDRESS; +import static java.lang.foreign.ValueLayout.JAVA_FLOAT; +import static java.lang.foreign.ValueLayout.JAVA_INT; +import static java.lang.foreign.ValueLayout.JAVA_LONG; + +import java.lang.foreign.Arena; +import java.lang.foreign.FunctionDescriptor; +import java.lang.foreign.Linker; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.SymbolLookup; +import java.lang.invoke.MethodHandle; +import java.util.NoSuchElementException; +import java.util.Objects; +import javax.annotation.Nonnull; + +final class PanamaJoltNativeLibrary implements JoltNativeLibrary { + + private static final Linker LINKER = Linker.nativeLinker(); + private static final int GRAVITY_COMPONENTS = 3; + + private final MethodHandle createSpace; + private final MethodHandle destroySpace; + private final MethodHandle step; + private final MethodHandle setGravity; + private final MethodHandle getGravity; + private final MethodHandle createBody; + private final MethodHandle removeBody; + private final MethodHandle containsBody; + private final MethodHandle bodySnapshot; + private final MethodHandle setBodyTransform; + private final MethodHandle setBodyPosition; + private final MethodHandle setBodyVelocity; + private final MethodHandle setBodyType; + private final MethodHandle setBodyDamping; + private final MethodHandle setBodyFriction; + private final MethodHandle setBodyRestitution; + private final MethodHandle setBodyCollisionFilter; + private final MethodHandle setBodySensor; + private final MethodHandle setBodyContinuousCollision; + private final MethodHandle isBodyContinuousCollisionEnabled; + private final MethodHandle activateBody; + private final MethodHandle sleepBody; + private final MethodHandle applyBodyImpulse; + private final MethodHandle applyBodyForce; + private final MethodHandle createJoint; + private final MethodHandle removeJoint; + private final MethodHandle raycastClosest; + private final MethodHandle raycastAll; + private final MethodHandle contacts; + private final MethodHandle contactCount; + private final MethodHandle bodyCount; + private final MethodHandle jointCount; + + private PanamaJoltNativeLibrary(@Nonnull SymbolLookup symbols) { + Objects.requireNonNull(symbols, "symbols"); + createSpace = downcall(symbols, + "impulse_jolt_create_space", + FunctionDescriptor.of(JAVA_LONG)); + destroySpace = downcall(symbols, + "impulse_jolt_destroy_space", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG)); + step = downcall(symbols, + "impulse_jolt_step", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_FLOAT)); + setGravity = downcall(symbols, + "impulse_jolt_set_gravity", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_FLOAT, JAVA_FLOAT, JAVA_FLOAT)); + getGravity = downcall(symbols, + "impulse_jolt_get_gravity", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, ADDRESS)); + createBody = downcall(symbols, + "impulse_jolt_create_body", + FunctionDescriptor.of(JAVA_LONG, + JAVA_LONG, + JAVA_INT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_INT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_INT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT)); + removeBody = downcall(symbols, + "impulse_jolt_remove_body", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG)); + containsBody = downcall(symbols, + "impulse_jolt_contains_body", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG)); + bodySnapshot = downcall(symbols, + "impulse_jolt_body_snapshot", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG, ADDRESS, ADDRESS)); + setBodyTransform = downcall(symbols, + "impulse_jolt_set_body_transform", + FunctionDescriptor.of(JAVA_INT, + JAVA_LONG, + JAVA_LONG, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT)); + setBodyPosition = downcall(symbols, + "impulse_jolt_set_body_position", + FunctionDescriptor.of(JAVA_INT, + JAVA_LONG, + JAVA_LONG, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT)); + setBodyVelocity = downcall(symbols, + "impulse_jolt_set_body_velocity", + FunctionDescriptor.of(JAVA_INT, + JAVA_LONG, + JAVA_LONG, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT)); + setBodyType = downcall(symbols, + "impulse_jolt_set_body_type", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG, JAVA_INT)); + setBodyDamping = downcall(symbols, + "impulse_jolt_set_body_damping", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG, JAVA_FLOAT, JAVA_FLOAT)); + setBodyFriction = downcall(symbols, + "impulse_jolt_set_body_friction", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG, JAVA_FLOAT)); + setBodyRestitution = downcall(symbols, + "impulse_jolt_set_body_restitution", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG, JAVA_FLOAT)); + setBodyCollisionFilter = downcall(symbols, + "impulse_jolt_set_body_collision_filter", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG, JAVA_INT, JAVA_INT)); + setBodySensor = downcall(symbols, + "impulse_jolt_set_body_sensor", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG, JAVA_INT)); + setBodyContinuousCollision = downcall(symbols, + "impulse_jolt_set_body_continuous_collision", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG, JAVA_INT)); + isBodyContinuousCollisionEnabled = downcall(symbols, + "impulse_jolt_is_body_continuous_collision_enabled", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG)); + activateBody = downcall(symbols, + "impulse_jolt_activate_body", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG)); + sleepBody = downcall(symbols, + "impulse_jolt_sleep_body", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG)); + applyBodyImpulse = downcall(symbols, + "impulse_jolt_apply_body_impulse", + FunctionDescriptor.of(JAVA_INT, + JAVA_LONG, + JAVA_LONG, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_INT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_INT)); + applyBodyForce = downcall(symbols, + "impulse_jolt_apply_body_force", + FunctionDescriptor.of(JAVA_INT, + JAVA_LONG, + JAVA_LONG, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_INT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_INT)); + createJoint = downcall(symbols, + "impulse_jolt_create_joint", + FunctionDescriptor.of(JAVA_LONG, + JAVA_LONG, + JAVA_INT, + JAVA_LONG, + JAVA_LONG, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_INT, + JAVA_FLOAT, + JAVA_FLOAT)); + removeJoint = downcall(symbols, + "impulse_jolt_remove_joint", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_LONG)); + raycastClosest = downcall(symbols, + "impulse_jolt_raycast_closest", + FunctionDescriptor.of(JAVA_INT, + JAVA_LONG, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + ADDRESS, + ADDRESS)); + raycastAll = downcall(symbols, + "impulse_jolt_raycast_all", + FunctionDescriptor.of(JAVA_INT, + JAVA_LONG, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_FLOAT, + JAVA_INT, + ADDRESS, + ADDRESS)); + contacts = downcall(symbols, + "impulse_jolt_contacts", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG, JAVA_INT, ADDRESS, ADDRESS)); + contactCount = downcall(symbols, + "impulse_jolt_contact_count", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG)); + bodyCount = downcall(symbols, + "impulse_jolt_body_count", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG)); + jointCount = downcall(symbols, + "impulse_jolt_joint_count", + FunctionDescriptor.of(JAVA_INT, JAVA_LONG)); + } + + @Nonnull + static PanamaJoltNativeLibrary open(@Nonnull SymbolLookup symbols) { + return new PanamaJoltNativeLibrary(symbols); + } + + @Override + public long createSpace() { + try { + return (long) createSpace.invokeExact(); + } catch (Throwable throwable) { + throw nativeFailure("create space", throwable); + } + } + + @Override + public void destroySpace(long spaceHandle) { + requireSuccess("destroy space", invokeStatus(destroySpace, spaceHandle)); + } + + @Override + public void step(long spaceHandle, float dt) { + requireSuccess("step space", invokeStatus(step, spaceHandle, dt)); + } + + @Override + public void setGravity(long spaceHandle, float x, float y, float z) { + requireSuccess("set gravity", invokeStatus(setGravity, spaceHandle, x, y, z)); + } + + @Override + public void getGravity(long spaceHandle, float[] out) { + if (out.length < GRAVITY_COMPONENTS) { + throw new IllegalArgumentException("Gravity output must have at least 3 entries"); + } + try (Arena arena = Arena.ofConfined()) { + MemorySegment output = arena.allocate(JAVA_FLOAT, GRAVITY_COMPONENTS); + requireSuccess("get gravity", invokeStatus(getGravity, spaceHandle, output)); + out[0] = output.getAtIndex(JAVA_FLOAT, 0); + out[1] = output.getAtIndex(JAVA_FLOAT, 1); + out[2] = output.getAtIndex(JAVA_FLOAT, 2); + } + } + + @Override + public long createBody(long spaceHandle, + int shapeTypeCode, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode, + float groundY, + float mass, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW) { + return invokeLong("create body", + createBody, + spaceHandle, + shapeTypeCode, + halfExtentX, + halfExtentY, + halfExtentZ, + radius, + halfHeight, + axisCode, + groundY, + mass, + bodyTypeCode, + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW); + } + + @Override + public void removeBody(long spaceHandle, long bodyHandle) { + requireSuccess("remove body", invokeStatus(removeBody, spaceHandle, bodyHandle)); + } + + @Override + public boolean containsBody(long spaceHandle, long bodyHandle) { + return invokeInt("contains body", containsBody, spaceHandle, bodyHandle) != 0; + } + + @Override + public boolean bodySnapshot(long spaceHandle, long bodyHandle, JoltBodySnapshot out) { + try (Arena arena = Arena.ofConfined()) { + MemorySegment floats = arena.allocate(JAVA_FLOAT, JoltBodySnapshot.FLOAT_FIELD_COUNT); + MemorySegment ints = arena.allocate(JAVA_INT, JoltBodySnapshot.INT_FIELD_COUNT); + int status = invokeStatus(bodySnapshot, spaceHandle, bodyHandle, floats, ints); + if (status == 0) { + return false; + } + readSnapshot(floats, ints, out); + return true; + } + } + + @Override + public void setBodyTransform(long spaceHandle, + long bodyHandle, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW) { + requireSuccess("set body transform", + invokeStatus("set body transform", + setBodyTransform, + spaceHandle, + bodyHandle, + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW)); + } + + @Override + public void setBodyPosition(long spaceHandle, long bodyHandle, float x, float y, float z) { + requireSuccess("set body position", + invokeStatus("set body position", setBodyPosition, spaceHandle, bodyHandle, x, y, z)); + } + + @Override + public void setBodyVelocity(long spaceHandle, + long bodyHandle, + float linearX, + float linearY, + float linearZ, + float angularX, + float angularY, + float angularZ) { + requireSuccess("set body velocity", + invokeStatus("set body velocity", + setBodyVelocity, + spaceHandle, + bodyHandle, + linearX, + linearY, + linearZ, + angularX, + angularY, + angularZ)); + } + + @Override + public void setBodyType(long spaceHandle, long bodyHandle, int bodyTypeCode) { + requireSuccess("set body type", + invokeStatus(setBodyType, spaceHandle, bodyHandle, bodyTypeCode)); + } + + @Override + public void setBodyDamping(long spaceHandle, + long bodyHandle, + float linearDamping, + float angularDamping) { + requireSuccess("set body damping", + invokeStatus("set body damping", + setBodyDamping, + spaceHandle, + bodyHandle, + linearDamping, + angularDamping)); + } + + @Override + public void setBodyFriction(long spaceHandle, long bodyHandle, float friction) { + requireSuccess("set body friction", + invokeStatus("set body friction", setBodyFriction, spaceHandle, bodyHandle, friction)); + } + + @Override + public void setBodyRestitution(long spaceHandle, long bodyHandle, float restitution) { + requireSuccess("set body restitution", + invokeStatus("set body restitution", + setBodyRestitution, + spaceHandle, + bodyHandle, + restitution)); + } + + @Override + public void setBodyCollisionFilter(long spaceHandle, long bodyHandle, int group, int mask) { + requireSuccess("set body collision filter", + invokeStatus("set body collision filter", + setBodyCollisionFilter, + spaceHandle, + bodyHandle, + group, + mask)); + } + + @Override + public void setBodySensor(long spaceHandle, long bodyHandle, boolean sensor) { + requireSuccess("set body sensor", + invokeStatus(setBodySensor, spaceHandle, bodyHandle, sensor ? 1 : 0)); + } + + @Override + public void setBodyContinuousCollision(long spaceHandle, long bodyHandle, boolean enabled) { + requireSuccess("set body continuous collision", + invokeStatus(setBodyContinuousCollision, spaceHandle, bodyHandle, enabled ? 1 : 0)); + } + + @Override + public boolean isBodyContinuousCollisionEnabled(long spaceHandle, long bodyHandle) { + return invokeInt("is body continuous collision enabled", + isBodyContinuousCollisionEnabled, + spaceHandle, + bodyHandle) != 0; + } + + @Override + public void activateBody(long spaceHandle, long bodyHandle) { + requireSuccess("activate body", invokeStatus(activateBody, spaceHandle, bodyHandle)); + } + + @Override + public void sleepBody(long spaceHandle, long bodyHandle) { + requireSuccess("sleep body", invokeStatus(sleepBody, spaceHandle, bodyHandle)); + } + + @Override + public void applyBodyImpulse(long spaceHandle, + long bodyHandle, + float x, + float y, + float z, + boolean hasOffset, + float offsetX, + float offsetY, + float offsetZ, + boolean torque) { + requireSuccess("apply body impulse", + invokeStatus("apply body impulse", + applyBodyImpulse, + spaceHandle, + bodyHandle, + x, + y, + z, + hasOffset ? 1 : 0, + offsetX, + offsetY, + offsetZ, + torque ? 1 : 0)); + } + + @Override + public void applyBodyForce(long spaceHandle, + long bodyHandle, + float x, + float y, + float z, + boolean hasOffset, + float offsetX, + float offsetY, + float offsetZ, + boolean torque) { + requireSuccess("apply body force", + invokeStatus("apply body force", + applyBodyForce, + spaceHandle, + bodyHandle, + x, + y, + z, + hasOffset ? 1 : 0, + offsetX, + offsetY, + offsetZ, + torque ? 1 : 0)); + } + + @Override + public long createJoint(long spaceHandle, + int jointTypeCode, + long bodyAHandle, + long bodyBHandle, + float anchorAX, + float anchorAY, + float anchorAZ, + float anchorBX, + float anchorBY, + float anchorBZ, + float axisX, + float axisY, + float axisZ, + float restLength, + float stiffness, + float damping, + float lowerLimit, + float upperLimit, + boolean motorEnabled, + float motorTargetVelocity, + float motorMaxForce) { + return invokeLong("create joint", + createJoint, + spaceHandle, + jointTypeCode, + bodyAHandle, + bodyBHandle, + anchorAX, + anchorAY, + anchorAZ, + anchorBX, + anchorBY, + anchorBZ, + axisX, + axisY, + axisZ, + restLength, + stiffness, + damping, + lowerLimit, + upperLimit, + motorEnabled ? 1 : 0, + motorTargetVelocity, + motorMaxForce); + } + + @Override + public void removeJoint(long spaceHandle, long jointHandle) { + requireSuccess("remove joint", invokeStatus(removeJoint, spaceHandle, jointHandle)); + } + + @Override + public int raycastClosest(long spaceHandle, + float fromX, + float fromY, + float fromZ, + float toX, + float toY, + float toZ, + long[] bodyHandleOut, + float[] hitOut) { + if (bodyHandleOut.length < 1) { + throw new IllegalArgumentException("Closest ray body handle output must have at least 1 entry"); + } + if (hitOut.length < JoltNativeLibrary.RAY_HIT_FLOAT_COUNT) { + throw new IllegalArgumentException("Closest ray hit output must have at least 8 entries"); + } + try (Arena arena = Arena.ofConfined()) { + MemorySegment bodyHandles = arena.allocate(JAVA_LONG, 1); + MemorySegment hits = arena.allocate(JAVA_FLOAT, JoltNativeLibrary.RAY_HIT_FLOAT_COUNT); + int count = invokeInt("raycast closest", + raycastClosest, + spaceHandle, + fromX, + fromY, + fromZ, + toX, + toY, + toZ, + bodyHandles, + hits); + if (count <= 0) { + return 0; + } + bodyHandleOut[0] = bodyHandles.getAtIndex(JAVA_LONG, 0); + readFloats(hits, hitOut, JoltNativeLibrary.RAY_HIT_FLOAT_COUNT); + return count; + } + } + + @Override + public int raycastAll(long spaceHandle, + float fromX, + float fromY, + float fromZ, + float toX, + float toY, + float toZ, + int maxHits, + long[] bodyHandles, + float[] hits) { + if (maxHits <= 0) { + return 0; + } + if (bodyHandles.length < maxHits) { + throw new IllegalArgumentException("Raycast body handle output is smaller than maxHits"); + } + int floatCount = maxHits * JoltNativeLibrary.RAY_HIT_FLOAT_COUNT; + if (hits.length < floatCount) { + throw new IllegalArgumentException("Raycast hit output is smaller than maxHits"); + } + try (Arena arena = Arena.ofConfined()) { + MemorySegment nativeBodyHandles = arena.allocate(JAVA_LONG, maxHits); + MemorySegment nativeHits = arena.allocate(JAVA_FLOAT, floatCount); + int count = invokeInt("raycast all", + raycastAll, + spaceHandle, + fromX, + fromY, + fromZ, + toX, + toY, + toZ, + maxHits, + nativeBodyHandles, + nativeHits); + int boundedCount = Math.clamp(count, 0, maxHits); + readLongs(nativeBodyHandles, bodyHandles, boundedCount); + readFloats(nativeHits, + hits, + boundedCount * JoltNativeLibrary.RAY_HIT_FLOAT_COUNT); + return count; + } + } + + @Override + public int contacts(long spaceHandle, int maxContacts, long[] bodyHandles, float[] contacts) { + if (maxContacts <= 0) { + return 0; + } + int bodyHandleCount = maxContacts * JoltNativeLibrary.CONTACT_BODY_HANDLE_COUNT; + if (bodyHandles.length < bodyHandleCount) { + throw new IllegalArgumentException("Contact body handle output is smaller than maxContacts"); + } + int floatCount = maxContacts * JoltNativeLibrary.CONTACT_FLOAT_COUNT; + if (contacts.length < floatCount) { + throw new IllegalArgumentException("Contact output is smaller than maxContacts"); + } + try (Arena arena = Arena.ofConfined()) { + MemorySegment nativeBodyHandles = arena.allocate(JAVA_LONG, bodyHandleCount); + MemorySegment nativeContacts = arena.allocate(JAVA_FLOAT, floatCount); + int count = invokeInt("contacts", + this.contacts, + spaceHandle, + maxContacts, + nativeBodyHandles, + nativeContacts); + int boundedCount = Math.clamp(count, 0, maxContacts); + readLongs(nativeBodyHandles, + bodyHandles, + boundedCount * JoltNativeLibrary.CONTACT_BODY_HANDLE_COUNT); + readFloats(nativeContacts, + contacts, + boundedCount * JoltNativeLibrary.CONTACT_FLOAT_COUNT); + return count; + } + } + + @Override + public int contactCount(long spaceHandle) { + return invokeInt(contactCount, spaceHandle); + } + + @Override + public int bodyCount(long spaceHandle) { + return invokeInt(bodyCount, spaceHandle); + } + + @Override + public int jointCount(long spaceHandle) { + return invokeInt(jointCount, spaceHandle); + } + + @Nonnull + private static MethodHandle downcall(@Nonnull SymbolLookup symbols, + @Nonnull String symbol, + @Nonnull FunctionDescriptor descriptor) { + try { + return LINKER.downcallHandle(symbols.findOrThrow(symbol), descriptor); + } catch (NoSuchElementException exception) { + throw new IllegalStateException("Jolt native symbol is missing: " + symbol, exception); + } + } + + private static int invokeStatus(@Nonnull MethodHandle handle, long spaceHandle) { + try { + return (int) handle.invokeExact(spaceHandle); + } catch (Throwable throwable) { + throw nativeFailure("invoke native status function", throwable); + } + } + + private static int invokeStatus(@Nonnull MethodHandle handle, long spaceHandle, float value) { + try { + return (int) handle.invokeExact(spaceHandle, value); + } catch (Throwable throwable) { + throw nativeFailure("invoke native status function", throwable); + } + } + + private static int invokeStatus(@Nonnull MethodHandle handle, + long spaceHandle, + float x, + float y, + float z) { + try { + return (int) handle.invokeExact(spaceHandle, x, y, z); + } catch (Throwable throwable) { + throw nativeFailure("invoke native status function", throwable); + } + } + + private static int invokeStatus(@Nonnull MethodHandle handle, + long spaceHandle, + @Nonnull MemorySegment output) { + try { + return (int) handle.invokeExact(spaceHandle, output); + } catch (Throwable throwable) { + throw nativeFailure("invoke native status function", throwable); + } + } + + private static int invokeStatus(@Nonnull MethodHandle handle, + long spaceHandle, + long bodyHandle, + @Nonnull MemorySegment floats, + @Nonnull MemorySegment ints) { + try { + return (int) handle.invokeExact(spaceHandle, bodyHandle, floats, ints); + } catch (Throwable throwable) { + throw nativeFailure("invoke native status function", throwable); + } + } + + private static int invokeStatus(@Nonnull MethodHandle handle, long spaceHandle, long bodyHandle) { + try { + return (int) handle.invokeExact(spaceHandle, bodyHandle); + } catch (Throwable throwable) { + throw nativeFailure("invoke native status function", throwable); + } + } + + private static int invokeStatus(@Nonnull MethodHandle handle, + long spaceHandle, + long bodyHandle, + int value) { + try { + return (int) handle.invokeExact(spaceHandle, bodyHandle, value); + } catch (Throwable throwable) { + throw nativeFailure("invoke native status function", throwable); + } + } + + private static int invokeStatus(@Nonnull String operation, + @Nonnull MethodHandle handle, + Object... args) { + try { + return (int) handle.invokeWithArguments(args); + } catch (Throwable throwable) { + throw nativeFailure(operation, throwable); + } + } + + private static int invokeInt(@Nonnull MethodHandle handle, long spaceHandle) { + try { + return (int) handle.invokeExact(spaceHandle); + } catch (Throwable throwable) { + throw nativeFailure("invoke native int function", throwable); + } + } + + private static int invokeInt(@Nonnull String operation, + @Nonnull MethodHandle handle, + Object... args) { + try { + return (int) handle.invokeWithArguments(args); + } catch (Throwable throwable) { + throw nativeFailure(operation, throwable); + } + } + + private static long invokeLong(@Nonnull String operation, + @Nonnull MethodHandle handle, + Object... args) { + try { + return (long) handle.invokeWithArguments(args); + } catch (Throwable throwable) { + throw nativeFailure(operation, throwable); + } + } + + private static void readSnapshot(@Nonnull MemorySegment floats, + @Nonnull MemorySegment ints, + @Nonnull JoltBodySnapshot out) { + out.positionX = floats.getAtIndex(JAVA_FLOAT, 0); + out.positionY = floats.getAtIndex(JAVA_FLOAT, 1); + out.positionZ = floats.getAtIndex(JAVA_FLOAT, 2); + out.rotationX = floats.getAtIndex(JAVA_FLOAT, 3); + out.rotationY = floats.getAtIndex(JAVA_FLOAT, 4); + out.rotationZ = floats.getAtIndex(JAVA_FLOAT, 5); + out.rotationW = floats.getAtIndex(JAVA_FLOAT, 6); + out.linearVelocityX = floats.getAtIndex(JAVA_FLOAT, 7); + out.linearVelocityY = floats.getAtIndex(JAVA_FLOAT, 8); + out.linearVelocityZ = floats.getAtIndex(JAVA_FLOAT, 9); + out.angularVelocityX = floats.getAtIndex(JAVA_FLOAT, 10); + out.angularVelocityY = floats.getAtIndex(JAVA_FLOAT, 11); + out.angularVelocityZ = floats.getAtIndex(JAVA_FLOAT, 12); + out.mass = floats.getAtIndex(JAVA_FLOAT, 13); + out.friction = floats.getAtIndex(JAVA_FLOAT, 14); + out.restitution = floats.getAtIndex(JAVA_FLOAT, 15); + out.linearDamping = floats.getAtIndex(JAVA_FLOAT, 16); + out.angularDamping = floats.getAtIndex(JAVA_FLOAT, 17); + out.centerOfMassOffsetY = floats.getAtIndex(JAVA_FLOAT, 18); + out.halfExtentX = floats.getAtIndex(JAVA_FLOAT, 19); + out.halfExtentY = floats.getAtIndex(JAVA_FLOAT, 20); + out.halfExtentZ = floats.getAtIndex(JAVA_FLOAT, 21); + out.radius = floats.getAtIndex(JAVA_FLOAT, 22); + out.halfHeight = floats.getAtIndex(JAVA_FLOAT, 23); + out.shapeTypeCode = ints.getAtIndex(JAVA_INT, 0); + out.bodyTypeCode = ints.getAtIndex(JAVA_INT, 1); + out.sleeping = ints.getAtIndex(JAVA_INT, 2) != 0; + out.sensor = ints.getAtIndex(JAVA_INT, 3) != 0; + out.collisionGroup = ints.getAtIndex(JAVA_INT, 4); + out.collisionMask = ints.getAtIndex(JAVA_INT, 5); + out.continuousCollisionEnabled = ints.getAtIndex(JAVA_INT, 6) != 0; + out.hasBoxHalfExtents = ints.getAtIndex(JAVA_INT, 7) != 0; + out.axisCode = ints.getAtIndex(JAVA_INT, 8); + } + + private static void readLongs(@Nonnull MemorySegment source, long[] target, int count) { + for (int index = 0; index < count; index++) { + target[index] = source.getAtIndex(JAVA_LONG, index); + } + } + + private static void readFloats(@Nonnull MemorySegment source, float[] target, int count) { + for (int index = 0; index < count; index++) { + target[index] = source.getAtIndex(JAVA_FLOAT, index); + } + } + + private static void requireSuccess(@Nonnull String operation, int status) { + if (status == 0) { + throw new IllegalStateException("Jolt native operation failed: " + operation); + } + } + + @Nonnull + private static IllegalStateException nativeFailure(@Nonnull String operation, + @Nonnull Throwable throwable) { + return new IllegalStateException("Failed to " + operation + " through Jolt native library", + throwable); + } +} diff --git a/impulse-backends/jolt/src/main/resources/META-INF/services/dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider b/impulse-backends/jolt/src/main/resources/META-INF/services/dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider new file mode 100644 index 00000000..e8fb8222 --- /dev/null +++ b/impulse-backends/jolt/src/main/resources/META-INF/services/dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider @@ -0,0 +1 @@ +dev.hytalemodding.impulse.jolt.JoltBackendRuntimeProvider diff --git a/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltBackendRuntimeContractTest.java b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltBackendRuntimeContractTest.java new file mode 100644 index 00000000..ae37d7e8 --- /dev/null +++ b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltBackendRuntimeContractTest.java @@ -0,0 +1,280 @@ +package dev.hytalemodding.impulse.jolt; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.capability.PhysicsActivationTuning; +import dev.hytalemodding.impulse.api.capability.PhysicsCapabilityId; +import dev.hytalemodding.impulse.api.capability.PhysicsSolverTuning; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import org.junit.jupiter.api.Test; + +class JoltBackendRuntimeContractTest { + + @Test + void validCreateBodyCodesReachDeferredBodyImplementation() { + JoltBackendRuntime runtime = runtimeWithSpace(1); + + int[] shapes = { + BackendRuntimeCodes.SHAPE_BOX, + BackendRuntimeCodes.SHAPE_SPHERE, + BackendRuntimeCodes.SHAPE_CAPSULE, + BackendRuntimeCodes.SHAPE_CYLINDER, + BackendRuntimeCodes.SHAPE_CONE, + BackendRuntimeCodes.SHAPE_PLANE + }; + int[] axes = { + BackendRuntimeCodes.AXIS_X, + BackendRuntimeCodes.AXIS_Y, + BackendRuntimeCodes.AXIS_Z + }; + int[] bodyTypes = { + BackendRuntimeCodes.BODY_STATIC, + BackendRuntimeCodes.BODY_DYNAMIC, + BackendRuntimeCodes.BODY_KINEMATIC + }; + + for (int shape : shapes) { + for (int axis : axes) { + for (int bodyType : bodyTypes) { + assertThrows(UnsupportedOperationException.class, + () -> createBody(runtime, 1, shape, axis, bodyType)); + } + } + } + } + + @Test + void invalidCreateBodyCodesFailBeforeDeferredBodyImplementation() { + JoltBackendRuntime runtime = runtimeWithSpace(2); + + assertThrows(IllegalArgumentException.class, + () -> createBody(runtime, 2, 999, BackendRuntimeCodes.AXIS_Y, BackendRuntimeCodes.BODY_DYNAMIC)); + assertThrows(IllegalArgumentException.class, + () -> createBody(runtime, 2, BackendRuntimeCodes.SHAPE_BOX, 999, BackendRuntimeCodes.BODY_DYNAMIC)); + assertThrows(IllegalArgumentException.class, + () -> createBody(runtime, 2, BackendRuntimeCodes.SHAPE_BOX, BackendRuntimeCodes.AXIS_Y, 999)); + } + + @Test + void unsupportedCreateBodyShapesFailAsContractInputErrors() { + JoltBackendRuntime runtime = runtimeWithSpace(3); + + assertThrows(IllegalArgumentException.class, + () -> createBody(runtime, + 3, + BackendRuntimeCodes.SHAPE_UNKNOWN, + BackendRuntimeCodes.AXIS_Y, + BackendRuntimeCodes.BODY_DYNAMIC)); + assertThrows(IllegalArgumentException.class, + () -> createBody(runtime, + 3, + BackendRuntimeCodes.SHAPE_VOXELS, + BackendRuntimeCodes.AXIS_Y, + BackendRuntimeCodes.BODY_STATIC)); + } + + @Test + void validCreateJointCodesReachEndpointValidation() { + JoltBackendRuntime runtime = runtimeWithSpace(4); + + int[] jointTypes = { + BackendRuntimeCodes.JOINT_FIXED, + BackendRuntimeCodes.JOINT_POINT, + BackendRuntimeCodes.JOINT_HINGE, + BackendRuntimeCodes.JOINT_SLIDER, + BackendRuntimeCodes.JOINT_SPRING + }; + + for (int jointType : jointTypes) { + assertThrows(IllegalArgumentException.class, + () -> createJoint(runtime, 4, jointType)); + } + } + + @Test + void invalidCreateJointCodeFailsBeforeDeferredJointImplementation() { + JoltBackendRuntime runtime = runtimeWithSpace(5); + + assertThrows(IllegalArgumentException.class, () -> createJoint(runtime, 5, 999)); + } + + @Test + void unsupportedCapabilityProbesReturnFalseAndSettingsMutationsNoop() { + JoltBackendRuntime runtime = runtimeWithSpace(6); + + assertFalse(runtime.supportsVoxelTerrain(6)); + assertTrue(runtime.supportsContinuousCollision(6)); + assertFalse(runtime.supportsSolverTuning(6)); + assertFalse(runtime.supportsActivationTuning(6)); + + assertDoesNotThrow(() -> runtime.applySolverTuning(6, new PhysicsSolverTuning(8, 2))); + assertDoesNotThrow(() -> runtime.applyActivationTuning(6, + new PhysicsActivationTuning(0.05f, 0.1f, 0.5f))); + assertDoesNotThrow(() -> runtime.applyExtensionSettings(6, + new PhysicsCapabilityId("impulse:test"), + consumer -> { + consumer.accept("key", "value"); + })); + } + + @Test + void unimplementedEmptyQueriesReturnEmptyResults() { + JoltBackendRuntime runtime = runtimeWithSpace(7); + + assertFalse(runtime.raycastClosest(7, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + -1.0f, + 0.0f, + (_, _, _, _, _, _, _, _, _) -> { + })); + assertEquals(0, + runtime.raycastAll(7, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + -1.0f, + 0.0f, + (_, _, _, _, _, _, _, _, _) -> { + })); + assertEquals(0, runtime.contacts(7, (_, _, _, _, _, _, _, _, _, _, _, _, _) -> { + })); + assertEquals(0, runtime.contactCount(7)); + } + + private static JoltBackendRuntime runtimeWithSpace(int spaceId) { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), new NoopNativeLibrary()); + runtime.createSpace(new SpaceId(spaceId)); + return runtime; + } + + private static long createBody(JoltBackendRuntime runtime, + int spaceId, + int shape, + int axis, + int bodyType) { + return runtime.createBody(spaceId, + shape, + 0.5f, + 0.5f, + 0.5f, + 0.25f, + 0.5f, + axis, + 0.0f, + 1.0f, + bodyType, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + } + + private static long createJoint(JoltBackendRuntime runtime, int spaceId, int jointType) { + return runtime.createJoint(spaceId, + jointType, + 1L, + 2L, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 1.0f, + 10.0f, + 0.5f, + -1.0f, + 1.0f, + false, + 0.0f, + 0.0f); + } + + private static final class NoopNativeLibrary implements JoltNativeLibrary { + + private long nextHandle = 1L; + + @Override + public long createSpace() { + return nextHandle++; + } + + @Override + public void destroySpace(long spaceHandle) { + } + + @Override + public void step(long spaceHandle, float dt) { + } + + @Override + public void setGravity(long spaceHandle, float x, float y, float z) { + } + + @Override + public void getGravity(long spaceHandle, float[] out) { + } + + @Override + public int bodyCount(long spaceHandle) { + return 0; + } + + @Override + public int raycastClosest(long spaceHandle, + float fromX, + float fromY, + float fromZ, + float toX, + float toY, + float toZ, + long[] bodyHandleOut, + float[] hitOut) { + return 0; + } + + @Override + public int raycastAll(long spaceHandle, + float fromX, + float fromY, + float fromZ, + float toX, + float toY, + float toZ, + int maxHits, + long[] bodyHandles, + float[] hits) { + return 0; + } + + @Override + public int contacts(long spaceHandle, int maxContacts, long[] bodyHandles, float[] contacts) { + return 0; + } + + @Override + public int contactCount(long spaceHandle) { + return 0; + } + + @Override + public int jointCount(long spaceHandle) { + return 0; + } + } +} diff --git a/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltBackendRuntimeProviderTest.java b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltBackendRuntimeProviderTest.java new file mode 100644 index 00000000..82f9f9fc --- /dev/null +++ b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltBackendRuntimeProviderTest.java @@ -0,0 +1,18 @@ +package dev.hytalemodding.impulse.jolt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import dev.hytalemodding.impulse.api.BackendId; +import org.junit.jupiter.api.Test; + +class JoltBackendRuntimeProviderTest { + + @Test + void providerExposesJoltBackendIdAndCreatesRuntime() { + JoltBackendRuntimeProvider provider = new JoltBackendRuntimeProvider(); + + assertEquals(new BackendId("impulse:jolt"), provider.getId()); + assertNotNull(provider.createRuntime()); + } +} diff --git a/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltBodyLifecycleTest.java b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltBodyLifecycleTest.java new file mode 100644 index 00000000..344c8eb7 --- /dev/null +++ b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltBodyLifecycleTest.java @@ -0,0 +1,83 @@ +package dev.hytalemodding.impulse.jolt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import org.junit.jupiter.api.Test; + +class JoltBodyLifecycleTest { + + @Test + void createBodyReturnsStableJavaBodyIdAndStoresNativeHandle() { + JoltTestNativeLibrary nativeLibrary = new JoltTestNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + int spaceId = runtime.createSpace(new SpaceId(11)); + + long bodyA = createBox(runtime, spaceId, 1.0f); + long bodyB = createBox(runtime, spaceId, 2.0f); + long spaceHandle = nativeLibrary.firstSpaceHandle(); + long nativeBodyA = nativeLibrary.nativeBodyHandle(spaceHandle, 0); + long nativeBodyB = nativeLibrary.nativeBodyHandle(spaceHandle, 1); + + assertEquals(1L, bodyA); + assertEquals(2L, bodyB); + assertNotEquals(nativeBodyA, bodyA); + assertNotEquals(nativeBodyB, bodyB); + assertEquals(2, runtime.bodyCount(spaceId)); + assertTrue(runtime.containsBody(spaceId, bodyA)); + assertTrue(runtime.containsBody(spaceId, bodyB)); + } + + @Test + void removeBodyDestroysNativeHandleAndMakesJavaIdStale() { + JoltTestNativeLibrary nativeLibrary = new JoltTestNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + int spaceId = runtime.createSpace(new SpaceId(12)); + long bodyId = createBox(runtime, spaceId, 1.0f); + long nativeBodyHandle = nativeLibrary.nativeBodyHandle(nativeLibrary.firstSpaceHandle(), 0); + + runtime.removeBody(spaceId, bodyId); + + assertEquals(nativeBodyHandle, nativeLibrary.lastRemovedBodyHandle()); + assertFalse(runtime.containsBody(spaceId, bodyId)); + assertEquals(0, runtime.bodyCount(spaceId)); + assertFalse(runtime.bodySnapshot(spaceId, + bodyId, + new JoltTestNativeLibrary.CapturedBodySnapshot())); + } + + @Test + void unknownBodyMutationFailsBeforeNativeCall() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), new JoltTestNativeLibrary()); + int spaceId = runtime.createSpace(new SpaceId(13)); + + assertThrows(IllegalArgumentException.class, + () -> runtime.setBodyPosition(spaceId, 404L, 1.0f, 2.0f, 3.0f)); + } + + static long createBox(JoltBackendRuntime runtime, int spaceId, float positionY) { + return runtime.createBody(spaceId, + BackendRuntimeCodes.SHAPE_BOX, + 0.5f, + 0.75f, + 1.25f, + 0.0f, + 0.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 2.0f, + BackendRuntimeCodes.BODY_DYNAMIC, + 1.0f, + positionY, + 3.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + } +} diff --git a/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltBodySnapshotTest.java b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltBodySnapshotTest.java new file mode 100644 index 00000000..5955c00f --- /dev/null +++ b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltBodySnapshotTest.java @@ -0,0 +1,85 @@ +package dev.hytalemodding.impulse.jolt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class JoltBodySnapshotTest { + + @Test + void bodySnapshotReportsNativeBodyStateWithJavaBodyId() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), new JoltTestNativeLibrary()); + int spaceId = runtime.createSpace(new SpaceId(21)); + long bodyId = JoltBodyLifecycleTest.createBox(runtime, spaceId, 1.0f); + + runtime.setBodyTransform(spaceId, bodyId, 2.0f, 3.0f, 4.0f, 0.1f, 0.2f, 0.3f, 0.9f); + runtime.setBodyVelocity(spaceId, bodyId, 5.0f, 6.0f, 7.0f, 0.5f, 0.6f, 0.7f); + runtime.setBodyType(spaceId, bodyId, BackendRuntimeCodes.BODY_KINEMATIC); + runtime.sleepBody(spaceId, bodyId); + + JoltTestNativeLibrary.CapturedBodySnapshot snapshot = + new JoltTestNativeLibrary.CapturedBodySnapshot(); + boolean present = runtime.bodySnapshot(spaceId, bodyId, snapshot); + + assertTrue(present); + assertEquals(bodyId, snapshot.bodyId); + assertEquals(BackendRuntimeCodes.SHAPE_BOX, snapshot.shapeTypeCode); + assertEquals(BackendRuntimeCodes.BODY_KINEMATIC, snapshot.bodyTypeCode); + assertEquals(2.0f, snapshot.positionX); + assertEquals(3.0f, snapshot.positionY); + assertEquals(4.0f, snapshot.positionZ); + assertEquals(0.1f, snapshot.rotationX); + assertEquals(0.2f, snapshot.rotationY); + assertEquals(0.3f, snapshot.rotationZ); + assertEquals(0.9f, snapshot.rotationW); + assertEquals(5.0f, snapshot.linearVelocityX); + assertEquals(6.0f, snapshot.linearVelocityY); + assertEquals(7.0f, snapshot.linearVelocityZ); + assertEquals(0.5f, snapshot.angularVelocityX); + assertEquals(0.6f, snapshot.angularVelocityY); + assertEquals(0.7f, snapshot.angularVelocityZ); + assertTrue(snapshot.sleeping); + assertEquals(2.0f, snapshot.mass); + assertTrue(snapshot.hasBoxHalfExtents); + assertEquals(0.5f, snapshot.halfExtentX); + assertEquals(0.75f, snapshot.halfExtentY); + assertEquals(1.25f, snapshot.halfExtentZ); + assertEquals(BackendRuntimeCodes.AXIS_Y, snapshot.axisCode); + } + + @Test + void snapshotBodiesEmitsOnlyKnownRequestedBodies() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), new JoltTestNativeLibrary()); + int spaceId = runtime.createSpace(new SpaceId(22)); + long bodyA = JoltBodyLifecycleTest.createBox(runtime, spaceId, 1.0f); + long bodyB = JoltBodyLifecycleTest.createBox(runtime, spaceId, 2.0f); + List emitted = new ArrayList<>(); + + runtime.snapshotBodies(spaceId, + consumer -> { + consumer.accept(bodyA); + consumer.accept(404L); + consumer.accept(bodyB); + }, + (bodyId, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) -> + emitted.add(bodyId)); + + assertEquals(List.of(bodyA, bodyB), emitted); + } + + @Test + void missingBodySnapshotReturnsFalse() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), new JoltTestNativeLibrary()); + int spaceId = runtime.createSpace(new SpaceId(23)); + + assertFalse(runtime.bodySnapshot(spaceId, + 404L, + new JoltTestNativeLibrary.CapturedBodySnapshot())); + } +} diff --git a/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltJointLifecycleTest.java b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltJointLifecycleTest.java new file mode 100644 index 00000000..3cd0e08d --- /dev/null +++ b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltJointLifecycleTest.java @@ -0,0 +1,178 @@ +package dev.hytalemodding.impulse.jolt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import org.junit.jupiter.api.Test; + +class JoltJointLifecycleTest { + + @Test + void createJointReturnsStableJavaJointIdAndStoresNativeHandle() { + JoltTestNativeLibrary nativeLibrary = new JoltTestNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + int spaceId = runtime.createSpace(new SpaceId(21)); + long bodyA = JoltBodyLifecycleTest.createBox(runtime, spaceId, 1.0f); + long bodyB = JoltBodyLifecycleTest.createBox(runtime, spaceId, 2.0f); + + long jointId = createJoint(runtime, spaceId, BackendRuntimeCodes.JOINT_POINT, bodyA, bodyB); + long spaceHandle = nativeLibrary.firstSpaceHandle(); + long nativeJointHandle = nativeLibrary.nativeJointHandle(spaceHandle, 0); + + assertEquals(1L, jointId); + assertNotEquals(nativeJointHandle, jointId); + assertEquals(1, runtime.jointCount(spaceId)); + assertEquals(BackendRuntimeCodes.JOINT_POINT, runtime.jointType(spaceId, jointId)); + assertEquals(bodyA, runtime.jointBodyA(spaceId, jointId)); + assertEquals(bodyB, runtime.jointBodyB(spaceId, jointId)); + } + + @Test + void createJointSupportsEveryBackendJointCode() { + JoltTestNativeLibrary nativeLibrary = new JoltTestNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + int spaceId = runtime.createSpace(new SpaceId(22)); + long bodyA = JoltBodyLifecycleTest.createBox(runtime, spaceId, 1.0f); + long bodyB = JoltBodyLifecycleTest.createBox(runtime, spaceId, 2.0f); + + int[] jointTypes = { + BackendRuntimeCodes.JOINT_FIXED, + BackendRuntimeCodes.JOINT_POINT, + BackendRuntimeCodes.JOINT_HINGE, + BackendRuntimeCodes.JOINT_SLIDER, + BackendRuntimeCodes.JOINT_SPRING + }; + + for (int index = 0; index < jointTypes.length; index++) { + long jointId = createJoint(runtime, spaceId, jointTypes[index], bodyA, bodyB); + + assertEquals(index + 1L, jointId); + assertEquals(jointTypes[index], runtime.jointType(spaceId, jointId)); + } + assertEquals(jointTypes.length, runtime.jointCount(spaceId)); + } + + @Test + void removeJointDestroysNativeHandleAndMakesJavaIdStale() { + JoltTestNativeLibrary nativeLibrary = new JoltTestNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + int spaceId = runtime.createSpace(new SpaceId(23)); + long bodyA = JoltBodyLifecycleTest.createBox(runtime, spaceId, 1.0f); + long bodyB = JoltBodyLifecycleTest.createBox(runtime, spaceId, 2.0f); + long jointId = createJoint(runtime, spaceId, BackendRuntimeCodes.JOINT_FIXED, bodyA, bodyB); + long nativeJointHandle = nativeLibrary.nativeJointHandle(nativeLibrary.firstSpaceHandle(), 0); + + runtime.removeJoint(spaceId, jointId); + + assertEquals(nativeJointHandle, nativeLibrary.lastRemovedJointHandle()); + assertEquals(0, runtime.jointCount(spaceId)); + assertThrows(IllegalArgumentException.class, () -> runtime.jointType(spaceId, jointId)); + assertThrows(IllegalArgumentException.class, () -> runtime.jointBodyA(spaceId, jointId)); + assertThrows(IllegalArgumentException.class, () -> runtime.jointBodyB(spaceId, jointId)); + } + + @Test + void removingBodyRemovesAttachedJointState() { + JoltTestNativeLibrary nativeLibrary = new JoltTestNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + int spaceId = runtime.createSpace(new SpaceId(25)); + long bodyA = JoltBodyLifecycleTest.createBox(runtime, spaceId, 1.0f); + long bodyB = JoltBodyLifecycleTest.createBox(runtime, spaceId, 2.0f); + long jointId = createJoint(runtime, spaceId, BackendRuntimeCodes.JOINT_FIXED, bodyA, bodyB); + + runtime.removeBody(spaceId, bodyA); + + assertEquals(0, runtime.jointCount(spaceId)); + assertEquals(0, nativeLibrary.jointCount(nativeLibrary.firstSpaceHandle())); + assertThrows(IllegalArgumentException.class, () -> runtime.jointType(spaceId, jointId)); + } + + @Test + void sameBodyJointEndpointsFailBeforeNativeCall() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), new JoltTestNativeLibrary()); + int spaceId = runtime.createSpace(new SpaceId(26)); + long body = JoltBodyLifecycleTest.createBox(runtime, spaceId, 1.0f); + + assertThrows(IllegalArgumentException.class, + () -> createJoint(runtime, spaceId, BackendRuntimeCodes.JOINT_POINT, body, body)); + } + + @Test + void hingeAndSliderSupportDefaultZeroLimits() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), new JoltTestNativeLibrary()); + int spaceId = runtime.createSpace(new SpaceId(27)); + long bodyA = JoltBodyLifecycleTest.createBox(runtime, spaceId, 1.0f); + long bodyB = JoltBodyLifecycleTest.createBox(runtime, spaceId, 2.0f); + + long hinge = createJoint(runtime, + spaceId, + BackendRuntimeCodes.JOINT_HINGE, + bodyA, + bodyB, + 0.0f, + 0.0f); + long slider = createJoint(runtime, + spaceId, + BackendRuntimeCodes.JOINT_SLIDER, + bodyA, + bodyB, + 0.0f, + 0.0f); + + assertEquals(1L, hinge); + assertEquals(2L, slider); + assertEquals(2, runtime.jointCount(spaceId)); + } + + @Test + void unknownJointMutationIsANoopAndLookupFails() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), new JoltTestNativeLibrary()); + int spaceId = runtime.createSpace(new SpaceId(24)); + + runtime.removeJoint(spaceId, 404L); + + assertEquals(0, runtime.jointCount(spaceId)); + assertThrows(IllegalArgumentException.class, () -> runtime.jointType(spaceId, 404L)); + } + + private static long createJoint(JoltBackendRuntime runtime, + int spaceId, + int jointType, + long bodyA, + long bodyB) { + return createJoint(runtime, spaceId, jointType, bodyA, bodyB, -1.0f, 1.0f); + } + + private static long createJoint(JoltBackendRuntime runtime, + int spaceId, + int jointType, + long bodyA, + long bodyB, + float lowerLimit, + float upperLimit) { + return runtime.createJoint(spaceId, + jointType, + bodyA, + bodyB, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 1.0f, + 10.0f, + 0.5f, + lowerLimit, + upperLimit, + true, + 0.5f, + 2.0f); + } +} diff --git a/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltMaterialAndFilterTest.java b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltMaterialAndFilterTest.java new file mode 100644 index 00000000..c86c2c99 --- /dev/null +++ b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltMaterialAndFilterTest.java @@ -0,0 +1,44 @@ +package dev.hytalemodding.impulse.jolt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.hytalemodding.impulse.api.SpaceId; +import org.junit.jupiter.api.Test; + +class JoltMaterialAndFilterTest { + + @Test + void materialFilterSensorAndActivationRoundTripThroughSnapshot() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), new JoltTestNativeLibrary()); + int spaceId = runtime.createSpace(new SpaceId(31)); + long bodyId = JoltBodyLifecycleTest.createBox(runtime, spaceId, 1.0f); + + runtime.setBodyDamping(spaceId, bodyId, 0.2f, 0.3f); + runtime.setBodyFriction(spaceId, bodyId, 0.65f); + runtime.setBodyRestitution(spaceId, bodyId, 0.15f); + runtime.setBodyCollisionFilter(spaceId, bodyId, 2, 3); + runtime.setBodySensor(spaceId, bodyId, true); + runtime.setBodyContinuousCollision(spaceId, bodyId, true); + runtime.sleepBody(spaceId, bodyId); + runtime.activateBody(spaceId, bodyId); + runtime.applyBodyImpulse(spaceId, bodyId, 1.0f, 0.0f, 0.0f, false, 0.0f, 0.0f, 0.0f, false); + runtime.applyBodyForce(spaceId, bodyId, 0.0f, 1.0f, 0.0f, false, 0.0f, 0.0f, 0.0f, false); + + JoltTestNativeLibrary.CapturedBodySnapshot snapshot = + new JoltTestNativeLibrary.CapturedBodySnapshot(); + runtime.bodySnapshot(spaceId, bodyId, snapshot); + + assertEquals(0.2f, snapshot.linearDamping); + assertEquals(0.3f, snapshot.angularDamping); + assertEquals(0.65f, snapshot.friction); + assertEquals(0.15f, snapshot.restitution); + assertEquals(2, snapshot.collisionGroup); + assertEquals(3, snapshot.collisionMask); + assertTrue(snapshot.sensor); + assertTrue(snapshot.continuousCollisionEnabled); + assertTrue(runtime.isBodyContinuousCollisionEnabled(spaceId, bodyId)); + assertFalse(snapshot.sleeping); + } +} diff --git a/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltNativeAbiIntegrationTest.java b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltNativeAbiIntegrationTest.java new file mode 100644 index 00000000..54da3b7e --- /dev/null +++ b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltNativeAbiIntegrationTest.java @@ -0,0 +1,331 @@ +package dev.hytalemodding.impulse.jolt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import org.junit.jupiter.api.Test; + +class JoltNativeAbiIntegrationTest { + + @Test + void nativeLibraryRunsSpaceAndBodyAbi() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend()); + int spaceId = runtime.createSpace(new SpaceId(41)); + runtime.setGravity(spaceId, 0.0f, -10.0f, 0.0f); + + long bodyId = runtime.createBody(spaceId, + BackendRuntimeCodes.SHAPE_SPHERE, + 0.0f, + 0.0f, + 0.0f, + 0.5f, + 0.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 2.0f, + BackendRuntimeCodes.BODY_DYNAMIC, + 0.0f, + 10.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + + runtime.setBodyFriction(spaceId, bodyId, 0.4f); + runtime.setBodyRestitution(spaceId, bodyId, 0.2f); + runtime.setBodyCollisionFilter(spaceId, bodyId, 3, 7); + runtime.setBodySensor(spaceId, bodyId, true); + runtime.setBodyVelocity(spaceId, bodyId, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f); + runtime.step(spaceId, 0.1f); + + JoltTestNativeLibrary.CapturedBodySnapshot snapshot = + new JoltTestNativeLibrary.CapturedBodySnapshot(); + assertTrue(runtime.bodySnapshot(spaceId, bodyId, snapshot)); + assertEquals(bodyId, snapshot.bodyId); + assertEquals(BackendRuntimeCodes.SHAPE_SPHERE, snapshot.shapeTypeCode); + assertEquals(BackendRuntimeCodes.BODY_DYNAMIC, snapshot.bodyTypeCode); + assertEquals(0.5f, snapshot.radius); + assertEquals(0.4f, snapshot.friction); + assertEquals(0.2f, snapshot.restitution); + assertEquals(3, snapshot.collisionGroup); + assertEquals(7, snapshot.collisionMask); + assertTrue(snapshot.sensor); + assertTrue(snapshot.positionY < 10.0f); + assertTrue(snapshot.linearVelocityY < 0.0f); + assertEquals(1, runtime.bodyCount(spaceId)); + + runtime.removeBody(spaceId, bodyId); + + assertFalse(runtime.containsBody(spaceId, bodyId)); + assertEquals(0, runtime.bodyCount(spaceId)); + assertFalse(runtime.bodySnapshot(spaceId, + bodyId, + new JoltTestNativeLibrary.CapturedBodySnapshot())); + + runtime.destroySpace(spaceId); + + assertThrows(IllegalArgumentException.class, () -> runtime.bodyCount(spaceId)); + } + + @Test + void nativeLibraryCreatesRestoredShapeAndBodyTypeCombinations() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend()); + int spaceId = runtime.createSpace(new SpaceId(43)); + int[] shapes = { + BackendRuntimeCodes.SHAPE_BOX, + BackendRuntimeCodes.SHAPE_SPHERE, + BackendRuntimeCodes.SHAPE_CAPSULE, + BackendRuntimeCodes.SHAPE_CYLINDER, + BackendRuntimeCodes.SHAPE_CONE, + BackendRuntimeCodes.SHAPE_PLANE + }; + int[] bodyTypes = { + BackendRuntimeCodes.BODY_STATIC, + BackendRuntimeCodes.BODY_DYNAMIC, + BackendRuntimeCodes.BODY_KINEMATIC + }; + + for (int shape : shapes) { + for (int bodyType : bodyTypes) { + long bodyId = assertDoesNotThrow(() -> runtime.createBody(spaceId, + shape, + 0.5f, + 0.5f, + 0.5f, + 0.5f, + 0.5f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + bodyType == BackendRuntimeCodes.BODY_DYNAMIC ? 1.0f : 0.0f, + bodyType, + 0.0f, + 2.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f), + "shape=" + shape + " bodyType=" + bodyType); + assertTrue(runtime.containsBody(spaceId, bodyId)); + runtime.removeBody(spaceId, bodyId); + } + } + + runtime.destroySpace(spaceId); + } + + @Test + void nativeLibraryCreatesBodiesPastPreviousRestoreScaleLimit() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend()); + int spaceId = runtime.createSpace(new SpaceId(44)); + int bodyCount = 65_537; + + for (int index = 0; index < bodyCount; index++) { + long bodyId = assertDoesNotThrow(() -> runtime.createBody(spaceId, + BackendRuntimeCodes.SHAPE_BOX, + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 0.0f, + BackendRuntimeCodes.BODY_STATIC, + 0.0f, + 2.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f), + "body index=" + index); + assertTrue(runtime.containsBody(spaceId, bodyId)); + } + + assertEquals(bodyCount, runtime.bodyCount(spaceId)); + runtime.destroySpace(spaceId); + } + + @Test + void nativeLibraryRunsJointAbiForEveryJointType() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend()); + int spaceId = runtime.createSpace(new SpaceId(42)); + long bodyA = runtime.createBody(spaceId, + BackendRuntimeCodes.SHAPE_BOX, + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 0.0f, + BackendRuntimeCodes.BODY_STATIC, + 0.0f, + 2.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + long bodyB = runtime.createBody(spaceId, + BackendRuntimeCodes.SHAPE_BOX, + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 1.0f, + BackendRuntimeCodes.BODY_DYNAMIC, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + + int[] jointTypes = { + BackendRuntimeCodes.JOINT_FIXED, + BackendRuntimeCodes.JOINT_POINT, + BackendRuntimeCodes.JOINT_HINGE, + BackendRuntimeCodes.JOINT_SLIDER, + BackendRuntimeCodes.JOINT_SPRING + }; + for (int jointType : jointTypes) { + long jointId = runtime.createJoint(spaceId, + jointType, + bodyA, + bodyB, + 0.0f, + -0.5f, + 0.0f, + 0.0f, + 0.5f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 1.0f, + 10.0f, + 0.5f, + -0.75f, + 0.75f, + true, + 0.25f, + 2.0f); + + assertEquals(1, runtime.jointCount(spaceId)); + assertEquals(1, nativeJointCount(runtime, spaceId)); + assertEquals(jointType, runtime.jointType(spaceId, jointId)); + assertEquals(bodyA, runtime.jointBodyA(spaceId, jointId)); + assertEquals(bodyB, runtime.jointBodyB(spaceId, jointId)); + + runtime.removeJoint(spaceId, jointId); + + assertEquals(0, runtime.jointCount(spaceId)); + assertEquals(0, nativeJointCount(runtime, spaceId)); + } + + runtime.destroySpace(spaceId); + } + + @Test + void nativeLibraryCreatesHingeAndSliderWithDefaultZeroLimits() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend()); + int spaceId = runtime.createSpace(new SpaceId(45)); + long bodyA = runtime.createBody(spaceId, + BackendRuntimeCodes.SHAPE_BOX, + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 0.0f, + BackendRuntimeCodes.BODY_STATIC, + 0.0f, + 2.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + long bodyB = runtime.createBody(spaceId, + BackendRuntimeCodes.SHAPE_BOX, + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 1.0f, + BackendRuntimeCodes.BODY_DYNAMIC, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + + for (int jointType : new int[] { + BackendRuntimeCodes.JOINT_HINGE, + BackendRuntimeCodes.JOINT_SLIDER + }) { + long jointId = assertDoesNotThrow(() -> runtime.createJoint(spaceId, + jointType, + bodyA, + bodyB, + 0.0f, + -0.5f, + 0.0f, + 0.0f, + 0.5f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 1.0f, + 10.0f, + 0.5f, + 0.0f, + 0.0f, + true, + 0.25f, + 2.0f)); + + assertEquals(jointType, runtime.jointType(spaceId, jointId)); + runtime.removeJoint(spaceId, jointId); + } + + runtime.destroySpace(spaceId); + } + + private static int nativeJointCount(JoltBackendRuntime runtime, int spaceId) { + int[] jointCount = new int[1]; + runtime.runtimeStats(spaceId, (_, + _, + _, + _, + _, + _, + _, + _, + _, + joints, + _) -> jointCount[0] = joints); + return jointCount[0]; + } +} diff --git a/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltNativePhysicsIntegrationTest.java b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltNativePhysicsIntegrationTest.java new file mode 100644 index 00000000..b3779067 --- /dev/null +++ b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltNativePhysicsIntegrationTest.java @@ -0,0 +1,67 @@ +package dev.hytalemodding.impulse.jolt; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import org.junit.jupiter.api.Test; + +class JoltNativePhysicsIntegrationTest { + + @Test + void dynamicBoxRestsOnStaticBoxFloor() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend()); + int spaceId = runtime.createSpace(new SpaceId(51)); + runtime.setGravity(spaceId, 0.0f, -9.81f, 0.0f); + runtime.createBody(spaceId, + BackendRuntimeCodes.SHAPE_BOX, + 20.0f, + 0.5f, + 20.0f, + 0.0f, + 0.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 0.0f, + BackendRuntimeCodes.BODY_STATIC, + 0.0f, + -0.5f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + long boxId = runtime.createBody(spaceId, + BackendRuntimeCodes.SHAPE_BOX, + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 1.0f, + BackendRuntimeCodes.BODY_DYNAMIC, + 0.0f, + 4.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + + for (int step = 0; step < 300; step++) { + runtime.step(spaceId, 1.0f / 60.0f); + } + + JoltTestNativeLibrary.CapturedBodySnapshot snapshot = + new JoltTestNativeLibrary.CapturedBodySnapshot(); + assertTrue(runtime.bodySnapshot(spaceId, boxId, snapshot)); + assertTrue(snapshot.positionY > 0.45f, + "dynamic box should rest on the static floor, y=" + snapshot.positionY); + assertTrue(snapshot.positionY < 0.75f, + "dynamic box should stay close to its resting height, y=" + snapshot.positionY); + + runtime.destroySpace(spaceId); + } +} diff --git a/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltNativeQueryIntegrationTest.java b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltNativeQueryIntegrationTest.java new file mode 100644 index 00000000..3e458e6d --- /dev/null +++ b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltNativeQueryIntegrationTest.java @@ -0,0 +1,222 @@ +package dev.hytalemodding.impulse.jolt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendContactSink; +import dev.hytalemodding.impulse.api.runtime.BackendRayHitSink; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class JoltNativeQueryIntegrationTest { + + @Test + void raycastClosestAndAllReturnBodyIdsInDistanceOrder() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend()); + int spaceId = runtime.createSpace(new SpaceId(63)); + long nearBody = createStaticBox(runtime, spaceId, 0.0f, 1.0f, -1.0f); + long farBody = createStaticBox(runtime, spaceId, 0.0f, 1.0f, 2.0f); + RayHitRecorder closest = new RayHitRecorder(); + RayHitRecorder all = new RayHitRecorder(); + + boolean hasHit = runtime.raycastClosest(spaceId, + 0.0f, + 1.0f, + -5.0f, + 0.0f, + 1.0f, + 5.0f, + closest); + int hitCount = runtime.raycastAll(spaceId, + 0.0f, + 1.0f, + -5.0f, + 0.0f, + 1.0f, + 5.0f, + all); + + assertTrue(hasHit); + assertEquals(nearBody, closest.firstBodyId()); + assertTrue(closest.firstDistance() > 0.0f); + assertEquals(2, hitCount); + assertEquals(List.of(nearBody, farBody), all.bodyIds()); + assertTrue(all.distances().get(0) < all.distances().get(1)); + + runtime.destroySpace(spaceId); + } + + @Test + void contactsExposeCurrentBodyPairAndRespectLimit() { + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend()); + int spaceId = runtime.createSpace(new SpaceId(64)); + runtime.setGravity(spaceId, 0.0f, -9.81f, 0.0f); + long floor = createStaticBox(runtime, spaceId, 0.0f, -0.5f, 0.0f, 20.0f, 0.5f, 20.0f); + long box = createDynamicBox(runtime, spaceId, 0.0f, 3.0f, 0.0f); + + for (int step = 0; step < 240 && runtime.contactCount(spaceId) == 0; step++) { + runtime.step(spaceId, 1.0f / 60.0f); + } + + ContactRecorder all = new ContactRecorder(); + int contactCount = runtime.contactCount(spaceId); + int emitted = runtime.contacts(spaceId, all); + ContactRecorder bounded = new ContactRecorder(); + int boundedEmitted = runtime.contacts(spaceId, 1, bounded); + + assertTrue(contactCount > 0); + assertEquals(contactCount, emitted); + assertTrue(all.containsPair(floor, box)); + assertEquals(1, boundedEmitted); + assertEquals(1, bounded.count()); + + runtime.destroySpace(spaceId); + } + + private static long createStaticBox(JoltBackendRuntime runtime, + int spaceId, + float positionX, + float positionY, + float positionZ) { + return createStaticBox(runtime, + spaceId, + positionX, + positionY, + positionZ, + 0.5f, + 0.5f, + 0.5f); + } + + private static long createStaticBox(JoltBackendRuntime runtime, + int spaceId, + float positionX, + float positionY, + float positionZ, + float halfExtentX, + float halfExtentY, + float halfExtentZ) { + return runtime.createBody(spaceId, + BackendRuntimeCodes.SHAPE_BOX, + halfExtentX, + halfExtentY, + halfExtentZ, + 0.0f, + 0.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 0.0f, + BackendRuntimeCodes.BODY_STATIC, + positionX, + positionY, + positionZ, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + } + + private static long createDynamicBox(JoltBackendRuntime runtime, + int spaceId, + float positionX, + float positionY, + float positionZ) { + return runtime.createBody(spaceId, + BackendRuntimeCodes.SHAPE_BOX, + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 1.0f, + BackendRuntimeCodes.BODY_DYNAMIC, + positionX, + positionY, + positionZ, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + } + + private static final class RayHitRecorder implements BackendRayHitSink { + + private final List bodyIds = new ArrayList<>(); + private final List distances = new ArrayList<>(); + + @Override + public void accept(long bodyId, + float pointX, + float pointY, + float pointZ, + float normalX, + float normalY, + float normalZ, + float fraction, + float distance) { + bodyIds.add(bodyId); + distances.add(distance); + } + + private long firstBodyId() { + return bodyIds.getFirst(); + } + + private float firstDistance() { + return distances.getFirst(); + } + + private List bodyIds() { + return bodyIds; + } + + private List distances() { + return distances; + } + } + + private static final class ContactRecorder implements BackendContactSink { + + private final List bodyAIds = new ArrayList<>(); + private final List bodyBIds = new ArrayList<>(); + + @Override + public void accept(long bodyAId, + long bodyBId, + float pointAX, + float pointAY, + float pointAZ, + float pointBX, + float pointBY, + float pointBZ, + float normalBX, + float normalBY, + float normalBZ, + float distance, + float impulse) { + bodyAIds.add(bodyAId); + bodyBIds.add(bodyBId); + } + + private boolean containsPair(long bodyAId, long bodyBId) { + for (int index = 0; index < bodyAIds.size(); index++) { + long actualA = bodyAIds.get(index); + long actualB = bodyBIds.get(index); + if ((actualA == bodyAId && actualB == bodyBId) + || (actualA == bodyBId && actualB == bodyAId)) { + return true; + } + } + return false; + } + + private int count() { + return bodyAIds.size(); + } + } +} diff --git a/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltQueryMappingTest.java b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltQueryMappingTest.java new file mode 100644 index 00000000..3c74d6cb --- /dev/null +++ b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltQueryMappingTest.java @@ -0,0 +1,155 @@ +package dev.hytalemodding.impulse.jolt; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendContactSink; +import dev.hytalemodding.impulse.api.runtime.BackendRayHitSink; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; + +class JoltQueryMappingTest { + + @Test + void raycastsMapNativeBodyHandlesBackToRuntimeBodyIds() { + JoltTestNativeLibrary nativeLibrary = new JoltTestNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + int spaceId = runtime.createSpace(new SpaceId(61)); + long bodyAId = JoltBodyLifecycleTest.createBox(runtime, spaceId, 1.0f); + long bodyBId = JoltBodyLifecycleTest.createBox(runtime, spaceId, 2.0f); + long spaceHandle = nativeLibrary.firstSpaceHandle(); + long bodyAHandle = nativeLibrary.nativeBodyHandle(spaceHandle, 0); + long bodyBHandle = nativeLibrary.nativeBodyHandle(spaceHandle, 1); + nativeLibrary.addRayHit(spaceHandle, bodyAHandle, 0.0f, 1.0f, -1.0f, 0.25f, 4.0f); + nativeLibrary.addRayHit(spaceHandle, bodyBHandle, 0.0f, 1.0f, 2.0f, 0.55f, 7.0f); + RayHitRecorder closest = new RayHitRecorder(); + RayHitRecorder all = new RayHitRecorder(); + + boolean hit = runtime.raycastClosest(spaceId, + 0.0f, + 1.0f, + -5.0f, + 0.0f, + 1.0f, + 5.0f, + closest); + int hitCount = runtime.raycastAll(spaceId, + 0.0f, + 1.0f, + -5.0f, + 0.0f, + 1.0f, + 5.0f, + all); + + assertTrue(hit); + assertEquals(bodyAId, closest.firstBodyId()); + assertEquals(0.25f, closest.firstFraction()); + assertEquals(2, hitCount); + assertEquals(List.of(bodyAId, bodyBId), all.bodyIds()); + } + + @Test + void contactsMapNativeBodyHandlesAndRespectLimit() { + JoltTestNativeLibrary nativeLibrary = new JoltTestNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + int spaceId = runtime.createSpace(new SpaceId(62)); + long bodyAId = JoltBodyLifecycleTest.createBox(runtime, spaceId, 1.0f); + long bodyBId = JoltBodyLifecycleTest.createBox(runtime, spaceId, 2.0f); + long spaceHandle = nativeLibrary.firstSpaceHandle(); + long bodyAHandle = nativeLibrary.nativeBodyHandle(spaceHandle, 0); + long bodyBHandle = nativeLibrary.nativeBodyHandle(spaceHandle, 1); + nativeLibrary.addContact(spaceHandle, bodyAHandle, bodyBHandle, 0.5f, -0.1f); + nativeLibrary.addContact(spaceHandle, bodyBHandle, bodyAHandle, 0.6f, -0.2f); + ContactRecorder contacts = new ContactRecorder(); + + int emitted = runtime.contacts(spaceId, 1, contacts); + + assertEquals(2, runtime.contactCount(spaceId)); + assertEquals(1, emitted); + assertEquals(1, contacts.count()); + assertEquals(bodyAId, contacts.firstBodyAId()); + assertEquals(bodyBId, contacts.firstBodyBId()); + assertEquals(-0.1f, contacts.firstDistance()); + } + + private static final class RayHitRecorder implements BackendRayHitSink { + + private final List bodyIds = new ArrayList<>(); + private final List fractions = new ArrayList<>(); + + @Override + public void accept(long bodyId, + float pointX, + float pointY, + float pointZ, + float normalX, + float normalY, + float normalZ, + float fraction, + float distance) { + bodyIds.add(bodyId); + fractions.add(fraction); + } + + private long firstBodyId() { + return bodyIds.getFirst(); + } + + private float firstFraction() { + return fractions.getFirst(); + } + + private List bodyIds() { + return bodyIds; + } + } + + private static final class ContactRecorder implements BackendContactSink { + + private long firstBodyAId; + private long firstBodyBId; + private float firstDistance; + private int count; + + @Override + public void accept(long bodyAId, + long bodyBId, + float pointAX, + float pointAY, + float pointAZ, + float pointBX, + float pointBY, + float pointBZ, + float normalBX, + float normalBY, + float normalBZ, + float distance, + float impulse) { + if (count == 0) { + firstBodyAId = bodyAId; + firstBodyBId = bodyBId; + firstDistance = distance; + } + count++; + } + + private long firstBodyAId() { + return firstBodyAId; + } + + private long firstBodyBId() { + return firstBodyBId; + } + + private float firstDistance() { + return firstDistance; + } + + private int count() { + return count; + } + } +} diff --git a/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltSpaceLifecycleTest.java b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltSpaceLifecycleTest.java new file mode 100644 index 00000000..eef60dd3 --- /dev/null +++ b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltSpaceLifecycleTest.java @@ -0,0 +1,220 @@ +package dev.hytalemodding.impulse.jolt; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import dev.hytalemodding.impulse.api.SpaceId; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class JoltSpaceLifecycleTest { + + @Test + void createSpaceUsesRequestedIdAndNativeHandle() { + InMemoryNativeLibrary nativeLibrary = new InMemoryNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + + int spaceId = runtime.createSpace(new SpaceId(42)); + + assertEquals(42, spaceId); + assertEquals(1, nativeLibrary.createCalls); + assertEquals(1L, nativeLibrary.handleFor(42)); + assertEquals(0, runtime.bodyCount(42)); + assertEquals(0, runtime.jointCount(42)); + } + + @Test + void gravityRoundTripUsesNativeSpaceHandle() { + InMemoryNativeLibrary nativeLibrary = new InMemoryNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + runtime.createSpace(new SpaceId(7)); + + runtime.setGravity(7, 0.0f, -4.0f, 1.0f); + float[] gravity = new float[3]; + runtime.getGravity(7, (x, y, z) -> { + gravity[0] = x; + gravity[1] = y; + gravity[2] = z; + }); + + assertArrayEquals(new float[] {0.0f, -4.0f, 1.0f}, gravity); + } + + @Test + void nonPositiveStepDoesNotCallNativeStep() { + InMemoryNativeLibrary nativeLibrary = new InMemoryNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + runtime.createSpace(new SpaceId(3)); + + runtime.step(3, 0.0f); + runtime.step(3, -1.0f); + + assertEquals(0, nativeLibrary.stepCalls); + } + + @Test + void nonPositiveStepStillRequiresKnownSpace() { + InMemoryNativeLibrary nativeLibrary = new InMemoryNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + + assertThrows(IllegalArgumentException.class, () -> runtime.step(404, 0.0f)); + assertEquals(0, nativeLibrary.stepCalls); + } + + @Test + void positiveStepCallsNativeStepWithHandle() { + InMemoryNativeLibrary nativeLibrary = new InMemoryNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + runtime.createSpace(new SpaceId(4)); + + runtime.step(4, 1.0f / 20.0f); + + assertEquals(1, nativeLibrary.stepCalls); + assertEquals(1L, nativeLibrary.lastStepHandle); + assertEquals(1.0f / 20.0f, nativeLibrary.lastStepDt); + } + + @Test + void destroySpaceReleasesNativeHandleAndRejectsFurtherAccess() { + InMemoryNativeLibrary nativeLibrary = new InMemoryNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + runtime.createSpace(new SpaceId(9)); + + runtime.destroySpace(9); + + assertEquals(1, nativeLibrary.destroyCalls); + assertEquals(1L, nativeLibrary.destroyedHandle); + assertThrows(IllegalArgumentException.class, () -> runtime.bodyCount(9)); + } + + @Test + void closeReleasesEveryRegisteredNativeSpace() { + InMemoryNativeLibrary nativeLibrary = new InMemoryNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + runtime.createSpace(new SpaceId(10)); + runtime.createSpace(new SpaceId(11)); + + runtime.close(); + + assertEquals(2, nativeLibrary.destroyCalls); + assertThrows(IllegalArgumentException.class, () -> runtime.bodyCount(10)); + assertThrows(IllegalArgumentException.class, () -> runtime.bodyCount(11)); + } + + @Test + void runtimeStatsReportsNativeCounts() { + InMemoryNativeLibrary nativeLibrary = new InMemoryNativeLibrary(); + JoltBackendRuntime runtime = new JoltBackendRuntime(new JoltBackend(), nativeLibrary); + runtime.createSpace(new SpaceId(6)); + nativeLibrary.space(1L).bodyCount = 12; + nativeLibrary.space(1L).jointCount = 5; + + int[] counts = new int[2]; + boolean[] available = new boolean[1]; + runtime.runtimeStats(6, (bodyCount, + _, + _, + _, + _, + _, + _, + _, + _, + jointCount, + statsAvailable) -> { + counts[0] = bodyCount; + counts[1] = jointCount; + available[0] = statsAvailable; + }); + + assertArrayEquals(new int[] {12, 5}, counts); + assertEquals(true, available[0]); + } + + private static final class InMemoryNativeLibrary implements JoltNativeLibrary { + + private final Map spaces = new HashMap<>(); + private long nextHandle = 1L; + private int createCalls; + private int destroyCalls; + private long destroyedHandle; + private int stepCalls; + private long lastStepHandle; + private float lastStepDt; + + @Override + public long createSpace() { + long handle = nextHandle++; + spaces.put(handle, new SpaceState()); + createCalls++; + return handle; + } + + @Override + public void destroySpace(long spaceHandle) { + spaces.remove(spaceHandle); + destroyedHandle = spaceHandle; + destroyCalls++; + } + + @Override + public void step(long spaceHandle, float dt) { + requireSpace(spaceHandle); + lastStepHandle = spaceHandle; + lastStepDt = dt; + stepCalls++; + } + + @Override + public void setGravity(long spaceHandle, float x, float y, float z) { + SpaceState space = requireSpace(spaceHandle); + space.gravity[0] = x; + space.gravity[1] = y; + space.gravity[2] = z; + } + + @Override + public void getGravity(long spaceHandle, float[] out) { + SpaceState space = requireSpace(spaceHandle); + out[0] = space.gravity[0]; + out[1] = space.gravity[1]; + out[2] = space.gravity[2]; + } + + @Override + public int bodyCount(long spaceHandle) { + return requireSpace(spaceHandle).bodyCount; + } + + @Override + public int jointCount(long spaceHandle) { + return requireSpace(spaceHandle).jointCount; + } + + private long handleFor(int spaceId) { + assertEquals(42, spaceId); + return spaces.keySet().iterator().next(); + } + + private SpaceState space(long handle) { + return requireSpace(handle); + } + + private SpaceState requireSpace(long spaceHandle) { + SpaceState space = spaces.get(spaceHandle); + if (space == null) { + throw new IllegalArgumentException("Unknown test native space handle: " + spaceHandle); + } + return space; + } + } + + private static final class SpaceState { + + private final float[] gravity = new float[] {0.0f, -9.81f, 0.0f}; + private int bodyCount; + private int jointCount; + } +} diff --git a/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltTestNativeLibrary.java b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltTestNativeLibrary.java new file mode 100644 index 00000000..24b9f3f0 --- /dev/null +++ b/impulse-backends/jolt/src/test/java/dev/hytalemodding/impulse/jolt/JoltTestNativeLibrary.java @@ -0,0 +1,710 @@ +package dev.hytalemodding.impulse.jolt; + +import dev.hytalemodding.impulse.api.runtime.BackendBodySnapshotSink; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +final class JoltTestNativeLibrary implements JoltNativeLibrary { + + private final Map spaces = new HashMap<>(); + private long nextSpaceHandle = 1L; + private long nextBodyHandle = 1001L; + private long nextJointHandle = 2001L; + private long lastRemovedBodyHandle; + private long lastRemovedJointHandle; + + @Override + public long createSpace() { + long handle = nextSpaceHandle++; + spaces.put(handle, new SpaceState()); + return handle; + } + + @Override + public void destroySpace(long spaceHandle) { + spaces.remove(spaceHandle); + } + + @Override + public void step(long spaceHandle, float dt) { + requireSpace(spaceHandle); + } + + @Override + public void setGravity(long spaceHandle, float x, float y, float z) { + SpaceState space = requireSpace(spaceHandle); + space.gravity[0] = x; + space.gravity[1] = y; + space.gravity[2] = z; + } + + @Override + public void getGravity(long spaceHandle, float[] out) { + SpaceState space = requireSpace(spaceHandle); + out[0] = space.gravity[0]; + out[1] = space.gravity[1]; + out[2] = space.gravity[2]; + } + + @Override + public long createBody(long spaceHandle, + int shapeTypeCode, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode, + float groundY, + float mass, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW) { + SpaceState space = requireSpace(spaceHandle); + long bodyHandle = nextBodyHandle++; + BodyState body = new BodyState(); + body.snapshot.shapeTypeCode = shapeTypeCode; + body.snapshot.bodyTypeCode = bodyTypeCode; + body.snapshot.positionX = positionX; + body.snapshot.positionY = positionY; + body.snapshot.positionZ = positionZ; + body.snapshot.rotationX = rotationX; + body.snapshot.rotationY = rotationY; + body.snapshot.rotationZ = rotationZ; + body.snapshot.rotationW = rotationW; + body.snapshot.mass = mass; + body.snapshot.centerOfMassOffsetY = centerOfMassOffsetY(shapeTypeCode, + halfExtentY, + radius, + halfHeight, + axisCode); + body.snapshot.hasBoxHalfExtents = halfExtentX > 0.0f + && halfExtentY > 0.0f + && halfExtentZ > 0.0f; + body.snapshot.halfExtentX = halfExtentX; + body.snapshot.halfExtentY = halfExtentY; + body.snapshot.halfExtentZ = halfExtentZ; + body.snapshot.radius = radius; + body.snapshot.halfHeight = halfHeight; + body.snapshot.axisCode = axisCode; + body.groundY = groundY; + space.bodies.put(bodyHandle, body); + return bodyHandle; + } + + @Override + public void removeBody(long spaceHandle, long bodyHandle) { + SpaceState space = requireSpace(spaceHandle); + space.bodies.remove(bodyHandle); + space.joints.values().removeIf(joint -> joint.bodyAHandle() == bodyHandle + || joint.bodyBHandle() == bodyHandle); + lastRemovedBodyHandle = bodyHandle; + } + + @Override + public boolean containsBody(long spaceHandle, long bodyHandle) { + return requireSpace(spaceHandle).bodies.containsKey(bodyHandle); + } + + @Override + public boolean bodySnapshot(long spaceHandle, long bodyHandle, JoltBodySnapshot out) { + BodyState body = requireSpace(spaceHandle).bodies.get(bodyHandle); + if (body == null) { + return false; + } + out.copyFrom(body.snapshot); + return true; + } + + @Override + public void setBodyTransform(long spaceHandle, + long bodyHandle, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW) { + JoltBodySnapshot snapshot = requireBody(spaceHandle, bodyHandle).snapshot; + snapshot.positionX = positionX; + snapshot.positionY = positionY; + snapshot.positionZ = positionZ; + snapshot.rotationX = rotationX; + snapshot.rotationY = rotationY; + snapshot.rotationZ = rotationZ; + snapshot.rotationW = rotationW; + } + + @Override + public void setBodyPosition(long spaceHandle, long bodyHandle, float x, float y, float z) { + JoltBodySnapshot snapshot = requireBody(spaceHandle, bodyHandle).snapshot; + snapshot.positionX = x; + snapshot.positionY = y; + snapshot.positionZ = z; + } + + @Override + public void setBodyVelocity(long spaceHandle, + long bodyHandle, + float linearX, + float linearY, + float linearZ, + float angularX, + float angularY, + float angularZ) { + JoltBodySnapshot snapshot = requireBody(spaceHandle, bodyHandle).snapshot; + snapshot.linearVelocityX = linearX; + snapshot.linearVelocityY = linearY; + snapshot.linearVelocityZ = linearZ; + snapshot.angularVelocityX = angularX; + snapshot.angularVelocityY = angularY; + snapshot.angularVelocityZ = angularZ; + } + + @Override + public void setBodyType(long spaceHandle, long bodyHandle, int bodyTypeCode) { + requireBody(spaceHandle, bodyHandle).snapshot.bodyTypeCode = bodyTypeCode; + } + + @Override + public void setBodyDamping(long spaceHandle, long bodyHandle, float linearDamping, float angularDamping) { + JoltBodySnapshot snapshot = requireBody(spaceHandle, bodyHandle).snapshot; + snapshot.linearDamping = linearDamping; + snapshot.angularDamping = angularDamping; + } + + @Override + public void setBodyFriction(long spaceHandle, long bodyHandle, float friction) { + requireBody(spaceHandle, bodyHandle).snapshot.friction = friction; + } + + @Override + public void setBodyRestitution(long spaceHandle, long bodyHandle, float restitution) { + requireBody(spaceHandle, bodyHandle).snapshot.restitution = restitution; + } + + @Override + public void setBodyCollisionFilter(long spaceHandle, long bodyHandle, int group, int mask) { + JoltBodySnapshot snapshot = requireBody(spaceHandle, bodyHandle).snapshot; + snapshot.collisionGroup = group; + snapshot.collisionMask = mask; + } + + @Override + public void setBodySensor(long spaceHandle, long bodyHandle, boolean sensor) { + requireBody(spaceHandle, bodyHandle).snapshot.sensor = sensor; + } + + @Override + public void setBodyContinuousCollision(long spaceHandle, long bodyHandle, boolean enabled) { + requireBody(spaceHandle, bodyHandle).snapshot.continuousCollisionEnabled = enabled; + } + + @Override + public boolean isBodyContinuousCollisionEnabled(long spaceHandle, long bodyHandle) { + return requireBody(spaceHandle, bodyHandle).snapshot.continuousCollisionEnabled; + } + + @Override + public void activateBody(long spaceHandle, long bodyHandle) { + requireBody(spaceHandle, bodyHandle).snapshot.sleeping = false; + } + + @Override + public void sleepBody(long spaceHandle, long bodyHandle) { + requireBody(spaceHandle, bodyHandle).snapshot.sleeping = true; + } + + @Override + public void applyBodyImpulse(long spaceHandle, + long bodyHandle, + float x, + float y, + float z, + boolean hasOffset, + float offsetX, + float offsetY, + float offsetZ, + boolean torque) { + requireBody(spaceHandle, bodyHandle).impulses++; + } + + @Override + public void applyBodyForce(long spaceHandle, + long bodyHandle, + float x, + float y, + float z, + boolean hasOffset, + float offsetX, + float offsetY, + float offsetZ, + boolean torque) { + requireBody(spaceHandle, bodyHandle).forces++; + } + + @Override + public int bodyCount(long spaceHandle) { + return requireSpace(spaceHandle).bodies.size(); + } + + @Override + public int jointCount(long spaceHandle) { + return requireSpace(spaceHandle).joints.size(); + } + + @Override + public long createJoint(long spaceHandle, + int jointTypeCode, + long bodyAHandle, + long bodyBHandle, + float anchorAX, + float anchorAY, + float anchorAZ, + float anchorBX, + float anchorBY, + float anchorBZ, + float axisX, + float axisY, + float axisZ, + float restLength, + float stiffness, + float damping, + float lowerLimit, + float upperLimit, + boolean motorEnabled, + float motorTargetVelocity, + float motorMaxForce) { + SpaceState space = requireSpace(spaceHandle); + requireBody(spaceHandle, bodyAHandle); + requireBody(spaceHandle, bodyBHandle); + long jointHandle = nextJointHandle++; + space.joints.put(jointHandle, + new JointState(jointTypeCode, bodyAHandle, bodyBHandle)); + return jointHandle; + } + + @Override + public void removeJoint(long spaceHandle, long jointHandle) { + requireSpace(spaceHandle).joints.remove(jointHandle); + lastRemovedJointHandle = jointHandle; + } + + @Override + public int raycastClosest(long spaceHandle, + float fromX, + float fromY, + float fromZ, + float toX, + float toY, + float toZ, + long[] bodyHandleOut, + float[] hitOut) { + SpaceState space = requireSpace(spaceHandle); + if (space.rayHits.isEmpty()) { + return 0; + } + RayHit hit = space.rayHits.getFirst(); + bodyHandleOut[0] = hit.bodyHandle; + hit.copyTo(hitOut, 0); + return 1; + } + + @Override + public int raycastAll(long spaceHandle, + float fromX, + float fromY, + float fromZ, + float toX, + float toY, + float toZ, + int maxHits, + long[] bodyHandles, + float[] hits) { + SpaceState space = requireSpace(spaceHandle); + int emitted = Math.min(Math.max(maxHits, 0), space.rayHits.size()); + for (int index = 0; index < emitted; index++) { + RayHit hit = space.rayHits.get(index); + bodyHandles[index] = hit.bodyHandle; + hit.copyTo(hits, index * 8); + } + return emitted; + } + + @Override + public int contacts(long spaceHandle, int maxContacts, long[] bodyHandles, float[] contacts) { + SpaceState space = requireSpace(spaceHandle); + int emitted = Math.min(Math.max(maxContacts, 0), space.contacts.size()); + for (int index = 0; index < emitted; index++) { + Contact contact = space.contacts.get(index); + bodyHandles[index * 2] = contact.bodyAHandle; + bodyHandles[index * 2 + 1] = contact.bodyBHandle; + contact.copyTo(contacts, index * 11); + } + return emitted; + } + + @Override + public int contactCount(long spaceHandle) { + return requireSpace(spaceHandle).contacts.size(); + } + + void addRayHit(long spaceHandle, + long bodyHandle, + float pointX, + float pointY, + float pointZ, + float fraction, + float distance) { + requireSpace(spaceHandle).rayHits.add(new RayHit(bodyHandle, + pointX, + pointY, + pointZ, + 0.0f, + 1.0f, + 0.0f, + fraction, + distance)); + } + + void addContact(long spaceHandle, + long bodyAHandle, + long bodyBHandle, + float pointY, + float distance) { + requireSpace(spaceHandle).contacts.add(new Contact(bodyAHandle, + bodyBHandle, + 0.0f, + pointY, + 0.0f, + 0.0f, + pointY, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + distance, + 0.0f)); + } + + long nativeBodyHandle(long spaceHandle, int index) { + return requireSpace(spaceHandle).bodies.keySet().stream() + .skip(index) + .findFirst() + .orElseThrow(); + } + + long nativeJointHandle(long spaceHandle, int index) { + return requireSpace(spaceHandle).joints.keySet().stream() + .skip(index) + .findFirst() + .orElseThrow(); + } + + long firstSpaceHandle() { + return spaces.keySet().iterator().next(); + } + + long lastRemovedBodyHandle() { + return lastRemovedBodyHandle; + } + + long lastRemovedJointHandle() { + return lastRemovedJointHandle; + } + + BodyState body(long spaceHandle, long bodyHandle) { + return requireBody(spaceHandle, bodyHandle); + } + + private SpaceState requireSpace(long spaceHandle) { + SpaceState space = spaces.get(spaceHandle); + if (space == null) { + throw new IllegalArgumentException("Unknown native space handle: " + spaceHandle); + } + return space; + } + + private BodyState requireBody(long spaceHandle, long bodyHandle) { + BodyState body = requireSpace(spaceHandle).bodies.get(bodyHandle); + if (body == null) { + throw new IllegalArgumentException("Unknown native body handle: " + bodyHandle); + } + return body; + } + + private static float centerOfMassOffsetY(int shapeTypeCode, + float halfExtentY, + float radius, + float halfHeight, + int axisCode) { + return switch (shapeTypeCode) { + case 1 -> halfExtentY; + case 2 -> radius; + case 3, 4 -> axisCode == 2 ? radius + halfHeight : radius; + case 5 -> axisCode == 2 ? halfHeight : radius; + default -> 0.0f; + }; + } + + private static final class SpaceState { + + private final Map bodies = new LinkedHashMap<>(); + private final Map joints = new LinkedHashMap<>(); + private final float[] gravity = new float[] {0.0f, -9.81f, 0.0f}; + private final List rayHits = new ArrayList<>(); + private final List contacts = new ArrayList<>(); + } + + private record JointState(int jointTypeCode, + long bodyAHandle, + long bodyBHandle) { + } + + static final class BodyState { + + private final JoltBodySnapshot snapshot = new JoltBodySnapshot(); + private float groundY; + private int impulses; + private int forces; + + JoltBodySnapshot snapshot() { + return snapshot; + } + + float groundY() { + return groundY; + } + + int impulses() { + return impulses; + } + + int forces() { + return forces; + } + } + + static final class CapturedBodySnapshot implements BackendBodySnapshotSink { + + long bodyId; + int shapeTypeCode; + int bodyTypeCode; + float positionX; + float positionY; + float positionZ; + float rotationX; + float rotationY; + float rotationZ; + float rotationW; + float linearVelocityX; + float linearVelocityY; + float linearVelocityZ; + float angularVelocityX; + float angularVelocityY; + float angularVelocityZ; + boolean sleeping; + boolean sensor; + float mass; + float friction; + float restitution; + float linearDamping; + float angularDamping; + int collisionGroup; + int collisionMask; + boolean continuousCollisionEnabled; + float centerOfMassOffsetY; + boolean hasBoxHalfExtents; + float halfExtentX; + float halfExtentY; + float halfExtentZ; + float radius; + float halfHeight; + int axisCode; + int captures; + + @Override + public void accept(long bodyId, + int shapeTypeCode, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW, + float linearVelocityX, + float linearVelocityY, + float linearVelocityZ, + float angularVelocityX, + float angularVelocityY, + float angularVelocityZ, + boolean sleeping, + boolean sensor, + float mass, + float friction, + float restitution, + float linearDamping, + float angularDamping, + int collisionGroup, + int collisionMask, + boolean continuousCollisionEnabled, + float centerOfMassOffsetY, + boolean hasBoxHalfExtents, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode) { + this.bodyId = bodyId; + this.shapeTypeCode = shapeTypeCode; + this.bodyTypeCode = bodyTypeCode; + this.positionX = positionX; + this.positionY = positionY; + this.positionZ = positionZ; + this.rotationX = rotationX; + this.rotationY = rotationY; + this.rotationZ = rotationZ; + this.rotationW = rotationW; + this.linearVelocityX = linearVelocityX; + this.linearVelocityY = linearVelocityY; + this.linearVelocityZ = linearVelocityZ; + this.angularVelocityX = angularVelocityX; + this.angularVelocityY = angularVelocityY; + this.angularVelocityZ = angularVelocityZ; + this.sleeping = sleeping; + this.sensor = sensor; + this.mass = mass; + this.friction = friction; + this.restitution = restitution; + this.linearDamping = linearDamping; + this.angularDamping = angularDamping; + this.collisionGroup = collisionGroup; + this.collisionMask = collisionMask; + this.continuousCollisionEnabled = continuousCollisionEnabled; + this.centerOfMassOffsetY = centerOfMassOffsetY; + this.hasBoxHalfExtents = hasBoxHalfExtents; + this.halfExtentX = halfExtentX; + this.halfExtentY = halfExtentY; + this.halfExtentZ = halfExtentZ; + this.radius = radius; + this.halfHeight = halfHeight; + this.axisCode = axisCode; + captures++; + } + } + + private static final class RayHit { + + private final long bodyHandle; + private final float pointX; + private final float pointY; + private final float pointZ; + private final float normalX; + private final float normalY; + private final float normalZ; + private final float fraction; + private final float distance; + + private RayHit(long bodyHandle, + float pointX, + float pointY, + float pointZ, + float normalX, + float normalY, + float normalZ, + float fraction, + float distance) { + this.bodyHandle = bodyHandle; + this.pointX = pointX; + this.pointY = pointY; + this.pointZ = pointZ; + this.normalX = normalX; + this.normalY = normalY; + this.normalZ = normalZ; + this.fraction = fraction; + this.distance = distance; + } + + private void copyTo(float[] values, int offset) { + values[offset] = pointX; + values[offset + 1] = pointY; + values[offset + 2] = pointZ; + values[offset + 3] = normalX; + values[offset + 4] = normalY; + values[offset + 5] = normalZ; + values[offset + 6] = fraction; + values[offset + 7] = distance; + } + } + + private static final class Contact { + + private final long bodyAHandle; + private final long bodyBHandle; + private final float pointAX; + private final float pointAY; + private final float pointAZ; + private final float pointBX; + private final float pointBY; + private final float pointBZ; + private final float normalBX; + private final float normalBY; + private final float normalBZ; + private final float distance; + private final float impulse; + + private Contact(long bodyAHandle, + long bodyBHandle, + float pointAX, + float pointAY, + float pointAZ, + float pointBX, + float pointBY, + float pointBZ, + float normalBX, + float normalBY, + float normalBZ, + float distance, + float impulse) { + this.bodyAHandle = bodyAHandle; + this.bodyBHandle = bodyBHandle; + this.pointAX = pointAX; + this.pointAY = pointAY; + this.pointAZ = pointAZ; + this.pointBX = pointBX; + this.pointBY = pointBY; + this.pointBZ = pointBZ; + this.normalBX = normalBX; + this.normalBY = normalBY; + this.normalBZ = normalBZ; + this.distance = distance; + this.impulse = impulse; + } + + private void copyTo(float[] values, int offset) { + values[offset] = pointAX; + values[offset + 1] = pointAY; + values[offset + 2] = pointAZ; + values[offset + 3] = pointBX; + values[offset + 4] = pointBY; + values[offset + 5] = pointBZ; + values[offset + 6] = normalBX; + values[offset + 7] = normalBY; + values[offset + 8] = normalBZ; + values[offset + 9] = distance; + values[offset + 10] = impulse; + } + } +} diff --git a/impulse-backends/native-loader/build.gradle.kts b/impulse-backends/native-loader/build.gradle.kts new file mode 100644 index 00000000..d302fbbc --- /dev/null +++ b/impulse-backends/native-loader/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + id("java-library") +} + +base { + archivesName.set("impulse-native-loader") +} diff --git a/impulse-native-loader/src/main/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryLoader.java b/impulse-backends/native-loader/src/main/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryLoader.java similarity index 100% rename from impulse-native-loader/src/main/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryLoader.java rename to impulse-backends/native-loader/src/main/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryLoader.java diff --git a/impulse-native-loader/src/main/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryResource.java b/impulse-backends/native-loader/src/main/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryResource.java similarity index 100% rename from impulse-native-loader/src/main/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryResource.java rename to impulse-backends/native-loader/src/main/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryResource.java diff --git a/impulse-native-loader/src/test/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryLoaderTest.java b/impulse-backends/native-loader/src/test/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryLoaderTest.java similarity index 100% rename from impulse-native-loader/src/test/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryLoaderTest.java rename to impulse-backends/native-loader/src/test/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryLoaderTest.java diff --git a/impulse-native-loader/src/test/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryResourceTest.java b/impulse-backends/native-loader/src/test/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryResourceTest.java similarity index 100% rename from impulse-native-loader/src/test/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryResourceTest.java rename to impulse-backends/native-loader/src/test/java/dev/hytalemodding/impulse/internal/nativelib/NativeLibraryResourceTest.java diff --git a/impulse-native-loader/src/test/resources/native/linux/x86_64/libfake.so b/impulse-backends/native-loader/src/test/resources/native/linux/x86_64/libfake.so similarity index 100% rename from impulse-native-loader/src/test/resources/native/linux/x86_64/libfake.so rename to impulse-backends/native-loader/src/test/resources/native/linux/x86_64/libfake.so diff --git a/impulse-rapier/build.gradle.kts b/impulse-backends/rapier/build.gradle.kts similarity index 98% rename from impulse-rapier/build.gradle.kts rename to impulse-backends/rapier/build.gradle.kts index 934e17c7..a76d33b1 100644 --- a/impulse-rapier/build.gradle.kts +++ b/impulse-backends/rapier/build.gradle.kts @@ -7,6 +7,10 @@ plugins { id("java-library") } +base { + archivesName.set("impulse-backend-rapier") +} + data class RapierBackendPlatform( val taskSuffix: String, val archiveClassifier: String, @@ -246,15 +250,15 @@ tasks.processResources { } dependencies { - api(project(":impulse-api")) + api(project(":impulse-backends:api")) - implementation(project(":impulse-native-loader")) + implementation(project(":impulse-backends:native-loader")) } fun runtimeClasspathWithoutBundledApi(): FileCollection { return configurations.runtimeClasspath.get() - .filter { file -> !file.name.startsWith("impulse-api-") } + .filter { file -> !file.name.startsWith("impulse-backend-api-") } } fun Jar.expandRuntimeClasspath(runtimeClasspath: FileCollection) { diff --git a/impulse-backends/rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierBackendRuntime.java b/impulse-backends/rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierBackendRuntime.java new file mode 100644 index 00000000..64e12956 --- /dev/null +++ b/impulse-backends/rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierBackendRuntime.java @@ -0,0 +1,1494 @@ +package dev.hytalemodding.impulse.rapier; + +import dev.hytalemodding.impulse.api.PhysicsAxis; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.capability.PhysicsActivationTuning; +import dev.hytalemodding.impulse.api.capability.PhysicsCapabilityId; +import dev.hytalemodding.impulse.api.capability.PhysicsSolverTuning; +import dev.hytalemodding.impulse.api.runtime.BackendBodyIdSource; +import dev.hytalemodding.impulse.api.runtime.BackendBodySnapshotSink; +import dev.hytalemodding.impulse.api.runtime.BackendContactSink; +import dev.hytalemodding.impulse.api.runtime.BackendExtensionSettingsSource; +import dev.hytalemodding.impulse.api.runtime.BackendJointType; +import dev.hytalemodding.impulse.api.runtime.BackendRayHitSink; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeStatsSink; +import dev.hytalemodding.impulse.api.runtime.BackendStepPhaseStatsSink; +import dev.hytalemodding.impulse.api.runtime.BackendVec3Sink; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import java.lang.ref.Cleaner; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +final class RapierBackendRuntime implements PhysicsBackendRuntime { + + private static final Cleaner CLEANER = Cleaner.create(); + private static final float DEFAULT_DYNAMIC_MASS = 1.0f; + private static final float DEFAULT_BODY_FRICTION = 0.5f; + private static final int DEFAULT_BODY_COLLISION_GROUP = 1; + private static final int DEFAULT_BODY_COLLISION_MASK = 1; + private static final int DEFAULT_SOLVER_ITERATIONS = 4; + private static final int DEFAULT_INTERNAL_PGS_ITERATIONS = 1; + private static final int DEFAULT_STABILIZATION_ITERATIONS = 1; + private static final int DEFAULT_MIN_ISLAND_SIZE = 128; + private static final int BODY_SNAPSHOT_FLOATS = 16; + private static final int CONTACT_FLOATS = 15; + private static final int RAY_HIT_FLOATS = 10; + private static final int RUNTIME_STATS_VALUES = 10; + private static final int STEP_PHASE_STATS_VALUES = 6; + private static final PhysicsCapabilityId RAPIER_SOLVER_EXTENSION_ID = + new PhysicsCapabilityId("impulse:rapier_solver"); + private static final String INTERNAL_PGS_ITERATIONS = "internalPgsIterations"; + private static final String MIN_ISLAND_SIZE = "minIslandSize"; + + private final Map spaces = new HashMap<>(); + private long nextBodyId = 1L; + private long nextJointId = 1L; + + @Override + public int createSpace(@Nonnull SpaceId requestedId) { + Objects.requireNonNull(requestedId, "requestedId"); + int spaceId = requestedId.value(); + if (spaces.containsKey(spaceId)) { + throw new IllegalArgumentException("Physics space id=" + requestedId + + " is already registered"); + } + long nativeSpaceHandle = RapierNative.createSpaceNative(); + if (nativeSpaceHandle == 0L) { + throw new IllegalStateException("Rapier returned a null native space handle"); + } + spaces.put(spaceId, new SpaceState(spaceId, nativeSpaceHandle)); + return spaceId; + } + + @Override + public void destroySpace(int spaceId) { + SpaceState state = spaces.remove(spaceId); + if (state != null) { + state.close(); + } + } + + @Override + public void close() { + RuntimeException failure = null; + for (Integer spaceId : new ArrayList<>(spaces.keySet())) { + try { + destroySpace(spaceId); + } catch (RuntimeException exception) { + if (failure == null) { + failure = exception; + } else { + failure.addSuppressed(exception); + } + } + } + if (failure != null) { + throw failure; + } + } + + @Override + public void step(int spaceId, float dt) { + if (dt <= 0.0f) { + return; + } + SpaceState state = requireSpace(spaceId); + if (!RapierNative.stepNative(state.nativeSpaceHandle, dt)) { + throw new IllegalStateException("Rapier native step failed"); + } + } + + @Override + public void setGravity(int spaceId, float x, float y, float z) { + SpaceState state = requireSpace(spaceId); + RapierNative.setGravityNative(state.nativeSpaceHandle, x, y, z); + } + + @Override + public void getGravity(int spaceId, @Nonnull BackendVec3Sink sink) { + Objects.requireNonNull(sink, "sink"); + SpaceState state = requireSpace(spaceId); + float[] out = new float[3]; + RapierNative.getGravityNative(state.nativeSpaceHandle, out); + sink.accept(out[0], out[1], out[2]); + } + + @Override + public long createBody(int spaceId, + int shapeTypeCode, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode, + float groundY, + float mass, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW) { + return createBodyInternal(spaceId, + shapeTypeCode, + halfExtentX, + halfExtentY, + halfExtentZ, + radius, + halfHeight, + axisCode, + groundY, + mass, + bodyTypeCode, + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW, + 0.0f, + 0.0f, + DEFAULT_BODY_FRICTION, + 0.0f, + DEFAULT_BODY_COLLISION_GROUP, + DEFAULT_BODY_COLLISION_MASK, + false, + false); + } + + @Override + public long createBodyWithInitialState(int spaceId, + int shapeTypeCode, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode, + float groundY, + float mass, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW, + float linearDamping, + float angularDamping, + float friction, + float restitution, + int collisionGroup, + int collisionMask, + boolean sensor, + boolean continuousCollisionEnabled) { + return createBodyInternal(spaceId, + shapeTypeCode, + halfExtentX, + halfExtentY, + halfExtentZ, + radius, + halfHeight, + axisCode, + groundY, + mass, + bodyTypeCode, + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW, + linearDamping, + angularDamping, + friction, + restitution, + collisionGroup, + collisionMask, + sensor, + continuousCollisionEnabled); + } + + private long createBodyInternal(int spaceId, + int shapeTypeCode, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode, + float groundY, + float mass, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW, + float linearDamping, + float angularDamping, + float friction, + float restitution, + int collisionGroup, + int collisionMask, + boolean sensor, + boolean continuousCollisionEnabled) { + SpaceState state = requireSpace(spaceId); + ShapeType shapeType = BackendRuntimeCodes.shapeType(shapeTypeCode); + if (shapeType == ShapeType.VOXELS || shapeType == ShapeType.UNKNOWN) { + throw new IllegalArgumentException("Unsupported Rapier body shape " + shapeType); + } + PhysicsAxis axis = BackendRuntimeCodes.axis(axisCode); + PhysicsBodyType bodyType = BackendRuntimeCodes.bodyType(bodyTypeCode); + float storedMass = adjustedMass(mass, bodyType); + int storedBodyTypeCode = adjustedBodyTypeCode(bodyType, storedMass); + BodyState body = BodyState.regular(nextBodyId++, + shapeTypeCode, + halfExtentX, + halfExtentY, + halfExtentZ, + radius, + halfHeight, + axisCode, + centerOfMassOffsetY(shapeType, axis, halfExtentY, radius, halfHeight), + storedMass, + storedBodyTypeCode, + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW); + body.linearDamping = linearDamping; + body.angularDamping = angularDamping; + body.friction = friction; + body.restitution = restitution; + body.collisionGroup = collisionGroup; + body.collisionMask = collisionMask; + body.sensor = sensor; + body.continuousCollisionEnabled = continuousCollisionEnabled; + long handle = RapierNative.addBodyNative(state.nativeSpaceHandle, + shapeType.ordinal(), + body.halfExtentX, + body.halfExtentY, + body.halfExtentZ, + body.radius, + body.halfHeight, + axis.index(), + BackendRuntimeCodes.bodyType(body.bodyTypeCode).ordinal(), + body.mass, + body.positionX, + body.positionY, + body.positionZ, + body.rotationX, + body.rotationY, + body.rotationZ, + body.rotationW, + body.linearVelocityX, + body.linearVelocityY, + body.linearVelocityZ, + body.angularVelocityX, + body.angularVelocityY, + body.angularVelocityZ, + body.friction, + body.restitution, + body.linearDamping, + body.angularDamping, + body.sensor, + body.collisionGroup, + body.collisionMask, + body.continuousCollisionEnabled); + attachBody(state, body, handle); + return body.bodyId; + } + + @Override + public boolean supportsVoxelTerrain(int spaceId) { + requireSpace(spaceId); + return true; + } + + @Override + public long createVoxelTerrain(int spaceId, + float voxelSizeX, + float voxelSizeY, + float voxelSizeZ, + @Nonnull int[] voxelCoordinates, + float positionX, + float positionY, + float positionZ, + float friction, + float restitution, + int collisionGroup, + int collisionMask) { + Objects.requireNonNull(voxelCoordinates, "voxelCoordinates"); + SpaceState state = requireSpace(spaceId); + BodyState body = BodyState.voxels(nextBodyId++, + voxelSizeX, + voxelSizeY, + voxelSizeZ, + positionX, + positionY, + positionZ, + friction, + restitution, + collisionGroup, + collisionMask); + long handle = RapierNative.addVoxelTerrainNative(state.nativeSpaceHandle, + voxelSizeX, + voxelSizeY, + voxelSizeZ, + voxelCoordinates, + positionX, + positionY, + positionZ, + friction, + restitution, + collisionGroup, + collisionMask); + attachBody(state, body, handle); + return body.bodyId; + } + + @Override + public void combineVoxelTerrains(int spaceId, + long bodyAId, + long bodyBId, + int shiftX, + int shiftY, + int shiftZ) { + SpaceState state = requireSpace(spaceId); + BodyState bodyA = requireBody(state, bodyAId); + BodyState bodyB = requireBody(state, bodyBId); + if (bodyA == bodyB) { + throw new IllegalArgumentException("Cannot combine a voxel terrain body with itself"); + } + requireVoxelTerrain(bodyA); + requireVoxelTerrain(bodyB); + if (!RapierNative.combineVoxelTerrainNative(state.nativeSpaceHandle, + bodyA.nativeBodyHandle, + bodyB.nativeBodyHandle, + shiftX, + shiftY, + shiftZ)) { + throw new IllegalStateException("Rapier native voxel terrain combine failed"); + } + } + + @Override + public void removeBody(int spaceId, long bodyId) { + SpaceState state = requireSpace(spaceId); + BodyState body = state.bodiesById.get(bodyId); + if (body == null) { + return; + } + RapierNative.removeBodyNative(state.nativeSpaceHandle, body.nativeBodyHandle); + state.bodiesById.remove(bodyId); + state.bodyIdsByNativeHandle.remove(body.nativeBodyHandle); + removeAttachedJointState(state, bodyId); + } + + @Override + public int bodyCount(int spaceId) { + return requireSpace(spaceId).bodiesById.size(); + } + + @Override + public boolean containsBody(int spaceId, long bodyId) { + return requireSpace(spaceId).bodiesById.containsKey(bodyId); + } + + @Override + public boolean bodySnapshot(int spaceId, long bodyId, @Nonnull BackendBodySnapshotSink sink) { + Objects.requireNonNull(sink, "sink"); + SpaceState state = requireSpace(spaceId); + BodyState body = state.bodiesById.get(bodyId); + if (body == null) { + return false; + } + state.ensureSnapshotCapacity(1); + state.snapshotBodyHandles[0] = body.nativeBodyHandle; + int written = RapierNative.snapshotBodiesNative(state.nativeSpaceHandle, + state.snapshotBodyHandles, + 1, + state.snapshotBodyData); + if (written > 0) { + body.updateFromNative(state.snapshotBodyData, 0); + } + body.emit(sink); + return true; + } + + @Override + public void snapshotBodies(int spaceId, + @Nonnull BackendBodyIdSource bodyIds, + @Nonnull BackendBodySnapshotSink sink) { + Objects.requireNonNull(bodyIds, "bodyIds"); + Objects.requireNonNull(sink, "sink"); + SpaceState state = requireSpace(spaceId); + SnapshotSelection selection = new SnapshotSelection(state); + bodyIds.forEachBodyId(selection::add); + if (selection.count == 0) { + return; + } + int written = RapierNative.snapshotBodiesNative(state.nativeSpaceHandle, + state.snapshotBodyHandles, + selection.count, + state.snapshotBodyData); + int limit = Math.clamp(written, 0, selection.count); + for (int index = 0; index < limit; index++) { + BodyState body = selection.bodies[index]; + body.updateFromNative(state.snapshotBodyData, index * BODY_SNAPSHOT_FLOATS); + body.emit(sink); + } + for (int index = limit; index < selection.count; index++) { + selection.bodies[index].emit(sink); + } + } + + @Override + public void setBodyTransform(int spaceId, + long bodyId, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW) { + SpaceState state = requireSpace(spaceId); + BodyState body = requireBody(state, bodyId); + NormalizedRotation rotation = normalizeRotation(rotationX, rotationY, rotationZ, rotationW); + RapierNative.setBodyPositionNative(state.nativeSpaceHandle, + body.nativeBodyHandle, + positionX, + positionY, + positionZ); + body.setPosition(positionX, positionY, positionZ); + RapierNative.setBodyRotationNative(state.nativeSpaceHandle, + body.nativeBodyHandle, + rotation.x, + rotation.y, + rotation.z, + rotation.w); + body.setRotation(rotation.x, rotation.y, rotation.z, rotation.w); + } + + @Override + public void setBodyPosition(int spaceId, long bodyId, float x, float y, float z) { + SpaceState state = requireSpace(spaceId); + BodyState body = requireBody(state, bodyId); + RapierNative.setBodyPositionNative(state.nativeSpaceHandle, body.nativeBodyHandle, x, y, z); + body.setPosition(x, y, z); + } + + @Override + public void setBodyVelocity(int spaceId, + long bodyId, + float linearX, + float linearY, + float linearZ, + float angularX, + float angularY, + float angularZ) { + SpaceState state = requireSpace(spaceId); + BodyState body = requireBody(state, bodyId); + RapierNative.setBodyLinearVelocityNative(state.nativeSpaceHandle, + body.nativeBodyHandle, + linearX, + linearY, + linearZ); + body.linearVelocityX = linearX; + body.linearVelocityY = linearY; + body.linearVelocityZ = linearZ; + RapierNative.setBodyAngularVelocityNative(state.nativeSpaceHandle, + body.nativeBodyHandle, + angularX, + angularY, + angularZ); + body.angularVelocityX = angularX; + body.angularVelocityY = angularY; + body.angularVelocityZ = angularZ; + } + + @Override + public void setBodyType(int spaceId, long bodyId, int bodyTypeCode) { + SpaceState state = requireSpace(spaceId); + BodyState body = requireBody(state, bodyId); + PhysicsBodyType bodyType = BackendRuntimeCodes.bodyType(bodyTypeCode); + float adjustedMass = adjustedMass(body.mass, bodyType); + if (Float.compare(adjustedMass, body.mass) != 0) { + RapierNative.setBodyMassNative(state.nativeSpaceHandle, + body.nativeBodyHandle, + adjustedMass); + body.mass = adjustedMass; + } + int adjustedBodyTypeCode = adjustedBodyTypeCode(bodyType, adjustedMass); + RapierNative.setBodyTypeNative(state.nativeSpaceHandle, + body.nativeBodyHandle, + BackendRuntimeCodes.bodyType(adjustedBodyTypeCode).ordinal()); + body.bodyTypeCode = adjustedBodyTypeCode; + } + + @Override + public void setBodyDamping(int spaceId, long bodyId, float linearDamping, float angularDamping) { + SpaceState state = requireSpace(spaceId); + BodyState body = requireBody(state, bodyId); + RapierNative.setBodyDampingNative(state.nativeSpaceHandle, + body.nativeBodyHandle, + linearDamping, + angularDamping); + body.linearDamping = linearDamping; + body.angularDamping = angularDamping; + } + + @Override + public void setBodyFriction(int spaceId, long bodyId, float friction) { + SpaceState state = requireSpace(spaceId); + BodyState body = requireBody(state, bodyId); + RapierNative.setBodyFrictionNative(state.nativeSpaceHandle, body.nativeBodyHandle, friction); + body.friction = friction; + } + + @Override + public void setBodyRestitution(int spaceId, long bodyId, float restitution) { + SpaceState state = requireSpace(spaceId); + BodyState body = requireBody(state, bodyId); + RapierNative.setBodyRestitutionNative(state.nativeSpaceHandle, + body.nativeBodyHandle, + restitution); + body.restitution = restitution; + } + + @Override + public void setBodyCollisionFilter(int spaceId, long bodyId, int group, int mask) { + SpaceState state = requireSpace(spaceId); + BodyState body = requireBody(state, bodyId); + RapierNative.setBodyCollisionFilterNative(state.nativeSpaceHandle, + body.nativeBodyHandle, + group, + mask); + body.collisionGroup = group; + body.collisionMask = mask; + } + + @Override + public void setBodySensor(int spaceId, long bodyId, boolean sensor) { + SpaceState state = requireSpace(spaceId); + BodyState body = requireBody(state, bodyId); + RapierNative.setBodySensorNative(state.nativeSpaceHandle, body.nativeBodyHandle, sensor); + body.sensor = sensor; + } + + @Override + public void setBodyContinuousCollision(int spaceId, long bodyId, boolean enabled) { + SpaceState state = requireSpace(spaceId); + BodyState body = requireBody(state, bodyId); + RapierNative.setBodyCcdNative(state.nativeSpaceHandle, body.nativeBodyHandle, enabled); + body.continuousCollisionEnabled = enabled; + } + + @Override + public boolean isBodyContinuousCollisionEnabled(int spaceId, long bodyId) { + SpaceState state = requireSpace(spaceId); + BodyState body = requireBody(state, bodyId); + body.continuousCollisionEnabled = + RapierNative.isBodyCcdNative(state.nativeSpaceHandle, body.nativeBodyHandle); + return body.continuousCollisionEnabled; + } + + @Override + public void activateBody(int spaceId, long bodyId) { + SpaceState state = requireSpace(spaceId); + RapierNative.activateBodyNative(state.nativeSpaceHandle, + requireBody(state, bodyId).nativeBodyHandle); + } + + @Override + public void sleepBody(int spaceId, long bodyId) { + SpaceState state = requireSpace(spaceId); + RapierNative.sleepBodyNative(state.nativeSpaceHandle, + requireBody(state, bodyId).nativeBodyHandle); + } + + @Override + public void applyBodyImpulse(int spaceId, + long bodyId, + float x, + float y, + float z, + boolean hasOffset, + float offsetX, + float offsetY, + float offsetZ, + boolean torque) { + SpaceState state = requireSpace(spaceId); + long handle = requireBody(state, bodyId).nativeBodyHandle; + if (torque) { + RapierNative.applyBodyTorqueImpulseNative(state.nativeSpaceHandle, handle, x, y, z); + } else if (hasOffset) { + RapierNative.applyBodyImpulseNative(state.nativeSpaceHandle, + handle, + x, + y, + z, + offsetX, + offsetY, + offsetZ); + } else { + RapierNative.applyBodyCentralImpulseNative(state.nativeSpaceHandle, handle, x, y, z); + } + } + + @Override + public void applyBodyForce(int spaceId, + long bodyId, + float x, + float y, + float z, + boolean hasOffset, + float offsetX, + float offsetY, + float offsetZ, + boolean torque) { + SpaceState state = requireSpace(spaceId); + long handle = requireBody(state, bodyId).nativeBodyHandle; + if (torque) { + RapierNative.applyBodyTorqueNative(state.nativeSpaceHandle, handle, x, y, z); + } else if (hasOffset) { + RapierNative.applyBodyForceNative(state.nativeSpaceHandle, + handle, + x, + y, + z, + offsetX, + offsetY, + offsetZ); + } else { + RapierNative.applyBodyCentralForceNative(state.nativeSpaceHandle, handle, x, y, z); + } + } + + @Override + public long createJoint(int spaceId, + int jointTypeCode, + long bodyAId, + long bodyBId, + float anchorAX, + float anchorAY, + float anchorAZ, + float anchorBX, + float anchorBY, + float anchorBZ, + float axisX, + float axisY, + float axisZ, + float restLength, + float stiffness, + float damping, + float lowerLimit, + float upperLimit, + boolean motorEnabled, + float motorTargetVelocity, + float motorMaxForce) { + SpaceState state = requireSpace(spaceId); + BodyState bodyA = requireBody(state, bodyAId); + BodyState bodyB = requireBody(state, bodyBId); + NormalizedAxis axis = normalizeAxis(axisX, axisY, axisZ); + long handle = RapierNative.addJointNative(state.nativeSpaceHandle, + BackendRuntimeCodes.jointType(jointTypeCode).ordinal(), + bodyA.nativeBodyHandle, + bodyB.nativeBodyHandle, + anchorAX, + anchorAY, + anchorAZ, + anchorBX, + anchorBY, + anchorBZ, + axis.x, + axis.y, + axis.z, + restLength, + stiffness, + damping); + if (handle == 0L) { + throw new IllegalStateException("Rapier returned a null native joint handle"); + } + BackendJointType type = BackendRuntimeCodes.jointType(jointTypeCode); + if (type == BackendJointType.HINGE || type == BackendJointType.SLIDER) { + try { + RapierNative.setJointLimitsNative(state.nativeSpaceHandle, + handle, + lowerLimit, + upperLimit); + RapierNative.setJointMotorNative(state.nativeSpaceHandle, + handle, + motorEnabled, + motorTargetVelocity, + motorMaxForce); + } catch (RuntimeException exception) { + try { + RapierNative.removeJointNative(state.nativeSpaceHandle, handle); + } catch (RuntimeException cleanupFailure) { + exception.addSuppressed(cleanupFailure); + } + throw exception; + } + } + long jointId = nextJointId++; + JointState joint = new JointState(jointId, + handle, + jointTypeCode, + bodyAId, + bodyBId); + state.jointsById.put(jointId, joint); + return jointId; + } + + @Override + public void removeJoint(int spaceId, long jointId) { + SpaceState state = requireSpace(spaceId); + JointState joint = state.jointsById.get(jointId); + if (joint != null) { + RapierNative.removeJointNative(state.nativeSpaceHandle, joint.nativeJointHandle); + state.jointsById.remove(jointId); + } + } + + @Override + public int jointCount(int spaceId) { + return requireSpace(spaceId).jointsById.size(); + } + + @Override + public int jointType(int spaceId, long jointId) { + return requireJoint(requireSpace(spaceId), jointId).jointTypeCode; + } + + @Override + public long jointBodyA(int spaceId, long jointId) { + return requireJoint(requireSpace(spaceId), jointId).bodyAId; + } + + @Override + public long jointBodyB(int spaceId, long jointId) { + return requireJoint(requireSpace(spaceId), jointId).bodyBId; + } + + @Override + public boolean raycastClosest(int spaceId, + float fromX, + float fromY, + float fromZ, + float toX, + float toY, + float toZ, + @Nonnull BackendRayHitSink sink) { + SpaceState state = requireSpace(spaceId); + float[] raw = RapierNative.raycastAllNative(state.nativeSpaceHandle, + fromX, + fromY, + fromZ, + toX, + toY, + toZ); + int closestOffset = -1; + float closestFraction = Float.POSITIVE_INFINITY; + for (int offset = 0; raw != null && offset + RAY_HIT_FLOATS <= raw.length; + offset += RAY_HIT_FLOATS) { + long bodyHandle = rawBitFloatPairToLong(raw[offset], raw[offset + 1]); + if (!state.bodyIdsByNativeHandle.containsKey(bodyHandle)) { + continue; + } + float fraction = raw[offset + 8]; + if (fraction < closestFraction) { + closestFraction = fraction; + closestOffset = offset; + } + } + if (closestOffset < 0) { + return false; + } + emitRayHit(state, raw, closestOffset, sink); + return true; + } + + @Override + public int raycastAll(int spaceId, + float fromX, + float fromY, + float fromZ, + float toX, + float toY, + float toZ, + @Nonnull BackendRayHitSink sink) { + SpaceState state = requireSpace(spaceId); + float[] raw = RapierNative.raycastAllNative(state.nativeSpaceHandle, + fromX, + fromY, + fromZ, + toX, + toY, + toZ); + int hits = 0; + for (int offset = 0; raw != null && offset + RAY_HIT_FLOATS <= raw.length; + offset += RAY_HIT_FLOATS) { + if (emitRayHit(state, raw, offset, sink)) { + hits++; + } + } + return hits; + } + + @Override + public int contacts(int spaceId, @Nonnull BackendContactSink sink) { + SpaceState state = requireSpace(spaceId); + return emitContacts(state, RapierNative.getContactsNative(state.nativeSpaceHandle), sink); + } + + @Override + public int contacts(int spaceId, int maxContacts, @Nonnull BackendContactSink sink) { + if (maxContacts <= 0) { + return 0; + } + SpaceState state = requireSpace(spaceId); + return emitContacts(state, + RapierNative.getContactsLimitedNative(state.nativeSpaceHandle, maxContacts), + sink); + } + + @Override + public int contactCount(int spaceId) { + SpaceState state = requireSpace(spaceId); + float[] raw = RapierNative.getContactsNative(state.nativeSpaceHandle); + return raw != null ? raw.length / CONTACT_FLOATS : 0; + } + + @Override + public void runtimeStats(int spaceId, @Nonnull BackendRuntimeStatsSink sink) { + SpaceState state = requireSpace(spaceId); + int[] values = RapierNative.getRuntimeStatsNative(state.nativeSpaceHandle); + if (values == null || values.length < RUNTIME_STATS_VALUES) { + sink.accept(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, false); + return; + } + sink.accept(values[0], + values[1], + values[2], + values[3], + values[4], + values[5], + values[6], + values[7], + values[8], + values[9], + true); + } + + @Override + public void resetStepPhaseStats(int spaceId) { + RapierNative.resetStepPhaseStatsNative(requireSpace(spaceId).nativeSpaceHandle); + } + + @Override + public void stepPhaseStats(int spaceId, @Nonnull BackendStepPhaseStatsSink sink) { + SpaceState state = requireSpace(spaceId); + long[] values = RapierNative.getStepPhaseStatsNative(state.nativeSpaceHandle); + if (values == null || values.length < STEP_PHASE_STATS_VALUES) { + sink.accept(0L, 0L, 0L, 0L, 0L, 0L, false); + return; + } + sink.accept(values[0], values[1], values[2], values[3], values[4], values[5], true); + } + + @Override + public boolean supportsContinuousCollision(int spaceId) { + requireSpace(spaceId); + return true; + } + + @Override + public boolean supportsSolverTuning(int spaceId) { + requireSpace(spaceId); + return true; + } + + @Override + public boolean supportsActivationTuning(int spaceId) { + requireSpace(spaceId); + return true; + } + + @Override + public void applySolverTuning(int spaceId, @Nonnull PhysicsSolverTuning tuning) { + Objects.requireNonNull(tuning, "tuning"); + SpaceState state = requireSpace(spaceId); + state.applySolverTuning(tuning.solverIterations(), + state.internalPgsIterations, + tuning.stabilizationIterations(), + state.minIslandSize); + state.solverIterations = tuning.solverIterations(); + state.stabilizationIterations = tuning.stabilizationIterations(); + } + + @Override + public void applyActivationTuning(int spaceId, @Nonnull PhysicsActivationTuning tuning) { + Objects.requireNonNull(tuning, "tuning"); + SpaceState state = requireSpace(spaceId); + RapierNative.setDynamicSleepTuningNative(state.nativeSpaceHandle, + tuning.linearSleepThreshold(), + tuning.angularSleepThreshold(), + tuning.timeUntilSleep()); + } + + @Override + public void applyExtensionSettings(int spaceId, + @Nonnull PhysicsCapabilityId capabilityId, + @Nonnull BackendExtensionSettingsSource settings) { + Objects.requireNonNull(capabilityId, "capabilityId"); + Objects.requireNonNull(settings, "settings"); + SpaceState state = requireSpace(spaceId); + if (!RAPIER_SOLVER_EXTENSION_ID.equals(capabilityId)) { + return; + } + int[] internalPgsIterations = { state.internalPgsIterations }; + int[] minIslandSize = { state.minIslandSize }; + settings.forEachSetting((key, value) -> { + if (INTERNAL_PGS_ITERATIONS.equals(key)) { + internalPgsIterations[0] = parsePositive(value, key); + } else if (MIN_ISLAND_SIZE.equals(key)) { + minIslandSize[0] = parsePositive(value, key); + } + }); + state.applySolverTuning(state.solverIterations, + internalPgsIterations[0], + state.stabilizationIterations, + minIslandSize[0]); + state.internalPgsIterations = internalPgsIterations[0]; + state.minIslandSize = minIslandSize[0]; + } + + @Nonnull + private SpaceState requireSpace(int spaceId) { + SpaceState state = spaces.get(spaceId); + if (state == null || state.closed) { + throw new IllegalArgumentException("Physics space id=" + spaceId + + " is not registered"); + } + return state; + } + + @Nonnull + private static BodyState requireBody(@Nonnull SpaceState state, long bodyId) { + BodyState body = state.bodiesById.get(bodyId); + if (body == null) { + throw new IllegalArgumentException("Physics body id=" + bodyId + + " is not registered in space " + state.spaceId); + } + return body; + } + + @Nonnull + private static JointState requireJoint(@Nonnull SpaceState state, long jointId) { + JointState joint = state.jointsById.get(jointId); + if (joint == null) { + throw new IllegalArgumentException("Physics joint id=" + jointId + + " is not registered"); + } + return joint; + } + + private static void requireVoxelTerrain(@Nonnull BodyState body) { + if (body.shapeTypeCode != BackendRuntimeCodes.SHAPE_VOXELS) { + throw new IllegalArgumentException("Body must be a voxel terrain"); + } + } + + private static void attachBody(@Nonnull SpaceState state, + @Nonnull BodyState body, + long nativeBodyHandle) { + if (nativeBodyHandle == 0L) { + throw new IllegalStateException("Rapier returned a null native body handle"); + } + body.nativeBodyHandle = nativeBodyHandle; + state.bodiesById.put(body.bodyId, body); + state.bodyIdsByNativeHandle.put(nativeBodyHandle, body.bodyId); + } + + private static void removeAttachedJointState(@Nonnull SpaceState state, long bodyId) { + Iterator> iterator = state.jointsById.entrySet().iterator(); + while (iterator.hasNext()) { + JointState joint = iterator.next().getValue(); + if (joint.bodyAId != bodyId && joint.bodyBId != bodyId) { + continue; + } + iterator.remove(); + } + } + + private static boolean emitRayHit(@Nonnull SpaceState state, + @Nullable float[] raw, + int offset, + @Nonnull BackendRayHitSink sink) { + if (raw == null || offset + RAY_HIT_FLOATS > raw.length) { + return false; + } + Long bodyId = state.bodyIdsByNativeHandle.get(rawBitFloatPairToLong(raw[offset], + raw[offset + 1])); + if (bodyId == null) { + return false; + } + sink.accept(bodyId, + raw[offset + 2], + raw[offset + 3], + raw[offset + 4], + raw[offset + 5], + raw[offset + 6], + raw[offset + 7], + raw[offset + 8], + raw[offset + 9]); + return true; + } + + private static int emitContacts(@Nonnull SpaceState state, + @Nullable float[] raw, + @Nonnull BackendContactSink sink) { + if (raw == null) { + return 0; + } + int contacts = 0; + for (int offset = 0; offset + CONTACT_FLOATS <= raw.length; offset += CONTACT_FLOATS) { + Long bodyAId = state.bodyIdsByNativeHandle.get(rawBitFloatPairToLong(raw[offset], + raw[offset + 1])); + Long bodyBId = state.bodyIdsByNativeHandle.get(rawBitFloatPairToLong(raw[offset + 2], + raw[offset + 3])); + if (bodyAId == null || bodyBId == null) { + continue; + } + sink.accept(bodyAId, + bodyBId, + raw[offset + 4], + raw[offset + 5], + raw[offset + 6], + raw[offset + 7], + raw[offset + 8], + raw[offset + 9], + raw[offset + 10], + raw[offset + 11], + raw[offset + 12], + raw[offset + 13], + raw[offset + 14]); + contacts++; + } + return contacts; + } + + private static long rawBitFloatPairToLong(float upper, float lower) { + long upperBits = Float.floatToRawIntBits(upper); + long lowerBits = Float.floatToRawIntBits(lower) & 0xFFFFFFFFL; + return (upperBits << 32) | lowerBits; + } + + private static int parsePositive(@Nonnull String value, @Nonnull String key) { + int parsed; + try { + parsed = Integer.parseInt(value); + } catch (NumberFormatException exception) { + throw new IllegalArgumentException("Rapier extension setting " + key + + " must be an integer", exception); + } + if (parsed < 1) { + throw new IllegalArgumentException("Rapier extension setting " + key + + " must be positive"); + } + return parsed; + } + + private static float adjustedMass(float mass, @Nonnull PhysicsBodyType bodyType) { + if (bodyType == PhysicsBodyType.STATIC) { + return 0.0f; + } + return mass <= 0.0f ? DEFAULT_DYNAMIC_MASS : mass; + } + + private static int adjustedBodyTypeCode(@Nonnull PhysicsBodyType bodyType, float mass) { + if (mass <= 0.0f) { + return BackendRuntimeCodes.BODY_STATIC; + } + return BackendRuntimeCodes.bodyTypeCode(bodyType); + } + + private static float centerOfMassOffsetY(@Nonnull ShapeType shapeType, + @Nonnull PhysicsAxis axis, + float halfExtentY, + float radius, + float halfHeight) { + return switch (shapeType) { + case BOX -> halfExtentY; + case SPHERE -> radius; + case CAPSULE -> axis == PhysicsAxis.Y ? halfHeight + radius : radius; + case CYLINDER, CONE -> axis == PhysicsAxis.Y ? halfHeight : radius; + case PLANE, VOXELS, UNKNOWN -> 0.0f; + }; + } + + @Nonnull + private static NormalizedRotation normalizeRotation(float x, float y, float z, float w) { + float lengthSquared = x * x + y * y + z * z + w * w; + if (lengthSquared == 0.0f) { + return new NormalizedRotation(0.0f, 0.0f, 0.0f, 1.0f); + } + float inverseLength = (float) (1.0 / Math.sqrt(lengthSquared)); + return new NormalizedRotation(x * inverseLength, + y * inverseLength, + z * inverseLength, + w * inverseLength); + } + + private record NormalizedRotation(float x, float y, float z, float w) { + } + + @Nonnull + private static NormalizedAxis normalizeAxis(float axisX, float axisY, float axisZ) { + float lengthSquared = axisX * axisX + axisY * axisY + axisZ * axisZ; + if (lengthSquared == 0.0f) { + return new NormalizedAxis(0.0f, 1.0f, 0.0f); + } + float inverseLength = (float) (1.0 / Math.sqrt(lengthSquared)); + return new NormalizedAxis(axisX * inverseLength, + axisY * inverseLength, + axisZ * inverseLength); + } + + private record NormalizedAxis(float x, float y, float z) { + } + + private static final class SnapshotSelection { + + private final SpaceState state; + private BodyState[] bodies = new BodyState[16]; + private int count; + + private SnapshotSelection(@Nonnull SpaceState state) { + this.state = state; + } + + private void add(long bodyId) { + BodyState body = state.bodiesById.get(bodyId); + if (body == null) { + return; + } + if (bodies.length <= count) { + BodyState[] grown = new BodyState[bodies.length * 2]; + System.arraycopy(bodies, 0, grown, 0, bodies.length); + bodies = grown; + } + state.ensureSnapshotCapacity(count + 1); + bodies[count] = body; + state.snapshotBodyHandles[count] = body.nativeBodyHandle; + count++; + } + } + + private static final class SpaceState { + + private final int spaceId; + private long nativeSpaceHandle; + private final Cleaner.Cleanable cleanable; + private final Map bodiesById = new HashMap<>(); + private final Map bodyIdsByNativeHandle = new HashMap<>(); + private final Map jointsById = new HashMap<>(); + private long[] snapshotBodyHandles = new long[0]; + private float[] snapshotBodyData = new float[0]; + private int solverIterations = DEFAULT_SOLVER_ITERATIONS; + private int internalPgsIterations = DEFAULT_INTERNAL_PGS_ITERATIONS; + private int stabilizationIterations = DEFAULT_STABILIZATION_ITERATIONS; + private int minIslandSize = DEFAULT_MIN_ISLAND_SIZE; + private boolean closed; + + private SpaceState(int spaceId, long nativeSpaceHandle) { + this.spaceId = spaceId; + this.nativeSpaceHandle = nativeSpaceHandle; + this.cleanable = CLEANER.register(this, new NativeSpaceCleanup(nativeSpaceHandle)); + } + + private void ensureSnapshotCapacity(int bodyCount) { + if (snapshotBodyHandles.length < bodyCount) { + int capacity = Math.max(bodyCount, Math.max(1, snapshotBodyHandles.length * 2)); + long[] grown = new long[capacity]; + System.arraycopy(snapshotBodyHandles, 0, grown, 0, snapshotBodyHandles.length); + snapshotBodyHandles = grown; + } + int floats = bodyCount * BODY_SNAPSHOT_FLOATS; + if (snapshotBodyData.length < floats) { + int capacity = Math.max(floats, Math.max(BODY_SNAPSHOT_FLOATS, + snapshotBodyData.length * 2)); + snapshotBodyData = new float[capacity]; + } + } + + private void applySolverTuning(int solverIterations, + int internalPgsIterations, + int stabilizationIterations, + int minIslandSize) { + RapierNative.setSolverTuningNative(nativeSpaceHandle, + solverIterations, + internalPgsIterations, + stabilizationIterations, + minIslandSize); + } + + private void close() { + if (closed) { + return; + } + closed = true; + bodiesById.clear(); + bodyIdsByNativeHandle.clear(); + jointsById.clear(); + cleanable.clean(); + nativeSpaceHandle = 0L; + } + } + + private static final class BodyState { + + private final long bodyId; + private long nativeBodyHandle; + private final int shapeTypeCode; + private final int axisCode; + private final float halfExtentX; + private final float halfExtentY; + private final float halfExtentZ; + private final float radius; + private final float halfHeight; + private final float centerOfMassOffsetY; + private int bodyTypeCode; + private float positionX; + private float positionY; + private float positionZ; + private float rotationX; + private float rotationY; + private float rotationZ; + private float rotationW; + private float linearVelocityX; + private float linearVelocityY; + private float linearVelocityZ; + private float angularVelocityX; + private float angularVelocityY; + private float angularVelocityZ; + private boolean sleeping; + private boolean sensor; + private float mass; + private float friction = DEFAULT_BODY_FRICTION; + private float restitution; + private float linearDamping; + private float angularDamping; + private int collisionGroup = DEFAULT_BODY_COLLISION_GROUP; + private int collisionMask = DEFAULT_BODY_COLLISION_MASK; + private boolean continuousCollisionEnabled; + + private BodyState(long bodyId, + int shapeTypeCode, + int axisCode, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + float centerOfMassOffsetY, + float mass, + int bodyTypeCode) { + this.bodyId = bodyId; + this.shapeTypeCode = shapeTypeCode; + this.axisCode = axisCode; + this.halfExtentX = halfExtentX; + this.halfExtentY = halfExtentY; + this.halfExtentZ = halfExtentZ; + this.radius = radius; + this.halfHeight = halfHeight; + this.centerOfMassOffsetY = centerOfMassOffsetY; + this.mass = mass; + this.bodyTypeCode = bodyTypeCode; + setRotation(0.0f, 0.0f, 0.0f, 1.0f); + } + + @Nonnull + private static BodyState regular(long bodyId, + int shapeTypeCode, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode, + float centerOfMassOffsetY, + float mass, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW) { + BodyState body = new BodyState(bodyId, + shapeTypeCode, + axisCode, + halfExtentX, + halfExtentY, + halfExtentZ, + radius, + halfHeight, + centerOfMassOffsetY, + mass, + bodyTypeCode); + body.setPosition(positionX, positionY, positionZ); + body.setRotation(rotationX, rotationY, rotationZ, rotationW); + return body; + } + + @Nonnull + private static BodyState voxels(long bodyId, + float voxelSizeX, + float voxelSizeY, + float voxelSizeZ, + float positionX, + float positionY, + float positionZ, + float friction, + float restitution, + int collisionGroup, + int collisionMask) { + BodyState body = new BodyState(bodyId, + BackendRuntimeCodes.SHAPE_VOXELS, + BackendRuntimeCodes.AXIS_Y, + voxelSizeX, + voxelSizeY, + voxelSizeZ, + -1.0f, + -1.0f, + 0.0f, + 0.0f, + BackendRuntimeCodes.BODY_STATIC); + body.setPosition(positionX, positionY, positionZ); + body.friction = friction; + body.restitution = restitution; + body.collisionGroup = collisionGroup; + body.collisionMask = collisionMask; + return body; + } + + private void setPosition(float x, float y, float z) { + positionX = x; + positionY = y; + positionZ = z; + } + + private void setRotation(float x, float y, float z, float w) { + NormalizedRotation rotation = normalizeRotation(x, y, z, w); + rotationX = rotation.x; + rotationY = rotation.y; + rotationZ = rotation.z; + rotationW = rotation.w; + } + + private void updateFromNative(@Nonnull float[] values, int offset) { + sleeping = values[offset + 14] != 0.0f; + positionX = values[offset]; + positionY = values[offset + 1]; + positionZ = values[offset + 2]; + setRotation(values[offset + 3], + values[offset + 4], + values[offset + 5], + values[offset + 6]); + linearVelocityX = values[offset + 7]; + linearVelocityY = values[offset + 8]; + linearVelocityZ = values[offset + 9]; + angularVelocityX = values[offset + 10]; + angularVelocityY = values[offset + 11]; + angularVelocityZ = values[offset + 12]; + int bodyTypeOrdinal = Math.round(values[offset + 13]); + PhysicsBodyType[] bodyTypes = PhysicsBodyType.values(); + if (bodyTypeOrdinal >= 0 && bodyTypeOrdinal < bodyTypes.length) { + bodyTypeCode = BackendRuntimeCodes.bodyTypeCode(bodyTypes[bodyTypeOrdinal]); + } + sensor = values[offset + 15] != 0.0f; + } + + private void emit(@Nonnull BackendBodySnapshotSink sink) { + sink.accept(bodyId, + shapeTypeCode, + bodyTypeCode, + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW, + linearVelocityX, + linearVelocityY, + linearVelocityZ, + angularVelocityX, + angularVelocityY, + angularVelocityZ, + sleeping, + sensor, + mass, + friction, + restitution, + linearDamping, + angularDamping, + collisionGroup, + collisionMask, + continuousCollisionEnabled, + centerOfMassOffsetY, + shapeTypeCode == BackendRuntimeCodes.SHAPE_BOX, + shapeTypeCode == BackendRuntimeCodes.SHAPE_BOX ? halfExtentX : 0.0f, + shapeTypeCode == BackendRuntimeCodes.SHAPE_BOX ? halfExtentY : 0.0f, + shapeTypeCode == BackendRuntimeCodes.SHAPE_BOX ? halfExtentZ : 0.0f, + radius, + halfHeight, + axisCode); + } + } + + private record JointState(long jointId, + long nativeJointHandle, + int jointTypeCode, + long bodyAId, + long bodyBId) { + } + + private static final class NativeSpaceCleanup implements Runnable { + + private final long nativeSpaceHandle; + + private NativeSpaceCleanup(long nativeSpaceHandle) { + this.nativeSpaceHandle = nativeSpaceHandle; + } + + @Override + public void run() { + RapierNative.destroySpaceNative(nativeSpaceHandle); + } + } +} diff --git a/impulse-backends/rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierBackendRuntimeProvider.java b/impulse-backends/rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierBackendRuntimeProvider.java new file mode 100644 index 00000000..4924d853 --- /dev/null +++ b/impulse-backends/rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierBackendRuntimeProvider.java @@ -0,0 +1,31 @@ +package dev.hytalemodding.impulse.rapier; + +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; +import javax.annotation.Nonnull; + +/** + * Runtime-provider service entry point for Rapier. + */ +public final class RapierBackendRuntimeProvider implements PhysicsBackendRuntimeProvider { + + private static final BackendId ID = new BackendId("impulse:rapier"); + + @Nonnull + @Override + public BackendId getId() { + return ID; + } + + @Override + public void init() { + RapierNative.load(); + } + + @Nonnull + @Override + public PhysicsBackendRuntime createRuntime() { + return new RapierBackendRuntime(); + } +} diff --git a/impulse-rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierNative.java b/impulse-backends/rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierNative.java similarity index 98% rename from impulse-rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierNative.java rename to impulse-backends/rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierNative.java index cce5de05..e0c0f6c7 100644 --- a/impulse-rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierNative.java +++ b/impulse-backends/rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierNative.java @@ -243,6 +243,8 @@ static native float[] raycastAllNative(long spaceHandle, static native float[] getContactsNative(long spaceHandle); + static native float[] getContactsLimitedNative(long spaceHandle, int maxContacts); + static native long addJointNative(long spaceHandle, int jointType, long bodyA, diff --git a/impulse-backends/rapier/src/main/resources/META-INF/services/dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider b/impulse-backends/rapier/src/main/resources/META-INF/services/dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider new file mode 100644 index 00000000..e0286d8a --- /dev/null +++ b/impulse-backends/rapier/src/main/resources/META-INF/services/dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider @@ -0,0 +1 @@ +dev.hytalemodding.impulse.rapier.RapierBackendRuntimeProvider diff --git a/impulse-rapier/src/main/rust/.gitignore b/impulse-backends/rapier/src/main/rust/.gitignore similarity index 100% rename from impulse-rapier/src/main/rust/.gitignore rename to impulse-backends/rapier/src/main/rust/.gitignore diff --git a/impulse-rapier/src/main/rust/Cargo.lock b/impulse-backends/rapier/src/main/rust/Cargo.lock similarity index 100% rename from impulse-rapier/src/main/rust/Cargo.lock rename to impulse-backends/rapier/src/main/rust/Cargo.lock diff --git a/impulse-rapier/src/main/rust/Cargo.toml b/impulse-backends/rapier/src/main/rust/Cargo.toml similarity index 100% rename from impulse-rapier/src/main/rust/Cargo.toml rename to impulse-backends/rapier/src/main/rust/Cargo.toml diff --git a/impulse-rapier/src/main/rust/patches/rapier3d-0.32.0-simd-body-masks.patch b/impulse-backends/rapier/src/main/rust/patches/rapier3d-0.32.0-simd-body-masks.patch similarity index 100% rename from impulse-rapier/src/main/rust/patches/rapier3d-0.32.0-simd-body-masks.patch rename to impulse-backends/rapier/src/main/rust/patches/rapier3d-0.32.0-simd-body-masks.patch diff --git a/impulse-rapier/src/main/rust/scripts/prepare-rapier-patched.ps1 b/impulse-backends/rapier/src/main/rust/scripts/prepare-rapier-patched.ps1 similarity index 100% rename from impulse-rapier/src/main/rust/scripts/prepare-rapier-patched.ps1 rename to impulse-backends/rapier/src/main/rust/scripts/prepare-rapier-patched.ps1 diff --git a/impulse-rapier/src/main/rust/scripts/prepare-rapier-patched.sh b/impulse-backends/rapier/src/main/rust/scripts/prepare-rapier-patched.sh similarity index 100% rename from impulse-rapier/src/main/rust/scripts/prepare-rapier-patched.sh rename to impulse-backends/rapier/src/main/rust/scripts/prepare-rapier-patched.sh diff --git a/impulse-rapier/src/main/rust/src/body_exports.rs b/impulse-backends/rapier/src/main/rust/src/body_exports.rs similarity index 100% rename from impulse-rapier/src/main/rust/src/body_exports.rs rename to impulse-backends/rapier/src/main/rust/src/body_exports.rs diff --git a/impulse-rapier/src/main/rust/src/joint_exports.rs b/impulse-backends/rapier/src/main/rust/src/joint_exports.rs similarity index 55% rename from impulse-rapier/src/main/rust/src/joint_exports.rs rename to impulse-backends/rapier/src/main/rust/src/joint_exports.rs index f191be35..1f22d391 100644 --- a/impulse-rapier/src/main/rust/src/joint_exports.rs +++ b/impulse-backends/rapier/src/main/rust/src/joint_exports.rs @@ -1,5 +1,49 @@ use super::*; +enum JointMutationFailure { + StaleJoint(jlong), + StaleImpulseJoint(jlong), +} + +fn throw_joint_mutation_failure( + env: &mut JNIEnv<'_>, + operation: &str, + failure: JointMutationFailure, +) { + let message = match failure { + JointMutationFailure::StaleJoint(joint_id) => { + format!("Rapier native {operation} failed: stale joint handle {joint_id}") + } + JointMutationFailure::StaleImpulseJoint(joint_id) => { + format!("Rapier native {operation} failed: stale impulse joint for joint {joint_id}") + } + }; + let _ = env.throw_new("java/lang/IllegalStateException", message); +} + +fn with_attached_joint_mutation( + env: &mut JNIEnv<'_>, + operation: &str, + space_handle: jlong, + joint_id: jlong, + f: F, +) where + F: FnOnce(&mut NativeSpace, JointEntry) -> Result<(), JointMutationFailure>, +{ + let result = with_space_checked(space_handle, |space| { + let entry = space + .joint(joint_id) + .ok_or(JointMutationFailure::StaleJoint(joint_id))?; + f(space, entry) + }); + + match result { + Ok(Ok(())) => {} + Ok(Err(failure)) => throw_joint_mutation_failure(env, operation, failure), + Err(failure) => throw_native_space_failure(env, operation, failure), + } +} + #[no_mangle] pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_addJointNative( _env: JNIEnv, @@ -101,35 +145,50 @@ pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_jointH #[no_mangle] pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_removeJointNative( - _env: JNIEnv, + mut env: JNIEnv, _class: JClass, space_handle: jlong, joint_id: jlong, ) { - with_space(space_handle, (), |space| { - if let Some(entry) = space.joints.remove(&joint_id) { + with_attached_joint_mutation( + &mut env, + "remove joint", + space_handle, + joint_id, + |space, entry| { + if space.impulse_joints.get(entry.joint).is_none() { + return Err(JointMutationFailure::StaleImpulseJoint(joint_id)); + } + space.joints.remove(&joint_id); space.impulse_joints.remove(entry.joint, true); - } - }); + Ok(()) + }, + ); } #[no_mangle] pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_setJointEnabledNative( - _env: JNIEnv, + mut env: JNIEnv, _class: JClass, space_handle: jlong, joint_id: jlong, enabled: jboolean, ) { - with_space(space_handle, (), |space| { - if let Some(mut entry) = space.joint(joint_id) { + with_attached_joint_mutation( + &mut env, + "set joint enabled", + space_handle, + joint_id, + |space, mut entry| { entry.enabled = bool_from_jboolean(enabled); - if let Some(joint) = space.impulse_joints.get_mut(entry.joint, true) { - joint.data.set_enabled(entry.enabled); - } + let Some(joint) = space.impulse_joints.get_mut(entry.joint, true) else { + return Err(JointMutationFailure::StaleImpulseJoint(joint_id)); + }; + joint.data.set_enabled(entry.enabled); space.joints.insert(joint_id, entry); - } - }); + Ok(()) + }, + ); } #[no_mangle] @@ -149,15 +208,19 @@ pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_isJoin #[no_mangle] pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_setJointLimitsNative( - _env: JNIEnv, + mut env: JNIEnv, _class: JClass, space_handle: jlong, joint_id: jlong, lower_limit: jfloat, upper_limit: jfloat, ) { - with_space(space_handle, (), |space| { - if let Some(entry) = space.joint(joint_id) { + with_attached_joint_mutation( + &mut env, + "set joint limits", + space_handle, + joint_id, + |space, entry| { let lower_limit = finite_or(lower_limit, 0.0); let upper_limit = finite_or(upper_limit, 0.0); let limits = if lower_limit <= upper_limit { @@ -165,24 +228,26 @@ pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_setJoi } else { [upper_limit, lower_limit] }; - if let Some(joint) = space.impulse_joints.get_mut(entry.joint, true) { - match entry.joint_type { - JOINT_HINGE => { - joint.data.set_limits(JointAxis::AngX, limits); - } - JOINT_SLIDER => { - joint.data.set_limits(JointAxis::LinX, limits); - } - _ => {} + let Some(joint) = space.impulse_joints.get_mut(entry.joint, true) else { + return Err(JointMutationFailure::StaleImpulseJoint(joint_id)); + }; + match entry.joint_type { + JOINT_HINGE => { + joint.data.set_limits(JointAxis::AngX, limits); + } + JOINT_SLIDER => { + joint.data.set_limits(JointAxis::LinX, limits); } + _ => {} } - } - }); + Ok(()) + }, + ); } #[no_mangle] pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_setJointMotorNative( - _env: JNIEnv, + mut env: JNIEnv, _class: JClass, space_handle: jlong, joint_id: jlong, @@ -190,35 +255,41 @@ pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_setJoi target_velocity: jfloat, max_force: jfloat, ) { - with_space(space_handle, (), |space| { - if let Some(entry) = space.joint(joint_id) { - if let Some(joint) = space.impulse_joints.get_mut(entry.joint, true) { - let target_velocity = if bool_from_jboolean(enabled) { - finite_or(target_velocity, 0.0) - } else { - 0.0 - }; - let max_force = if bool_from_jboolean(enabled) { - finite_nonnegative(max_force) - } else { - 0.0 - }; - match entry.joint_type { - JOINT_HINGE => { - joint - .data - .set_motor_velocity(JointAxis::AngX, target_velocity, 1.0); - joint.data.set_motor_max_force(JointAxis::AngX, max_force); - } - JOINT_SLIDER => { - joint - .data - .set_motor_velocity(JointAxis::LinX, target_velocity, 1.0); - joint.data.set_motor_max_force(JointAxis::LinX, max_force); - } - _ => {} + with_attached_joint_mutation( + &mut env, + "set joint motor", + space_handle, + joint_id, + |space, entry| { + let Some(joint) = space.impulse_joints.get_mut(entry.joint, true) else { + return Err(JointMutationFailure::StaleImpulseJoint(joint_id)); + }; + let target_velocity = if bool_from_jboolean(enabled) { + finite_or(target_velocity, 0.0) + } else { + 0.0 + }; + let max_force = if bool_from_jboolean(enabled) { + finite_nonnegative(max_force) + } else { + 0.0 + }; + match entry.joint_type { + JOINT_HINGE => { + joint + .data + .set_motor_velocity(JointAxis::AngX, target_velocity, 1.0); + joint.data.set_motor_max_force(JointAxis::AngX, max_force); + } + JOINT_SLIDER => { + joint + .data + .set_motor_velocity(JointAxis::LinX, target_velocity, 1.0); + joint.data.set_motor_max_force(JointAxis::LinX, max_force); } + _ => {} } - } - }); + Ok(()) + }, + ); } diff --git a/impulse-rapier/src/main/rust/src/lib.rs b/impulse-backends/rapier/src/main/rust/src/lib.rs similarity index 100% rename from impulse-rapier/src/main/rust/src/lib.rs rename to impulse-backends/rapier/src/main/rust/src/lib.rs diff --git a/impulse-rapier/src/main/rust/src/query_exports.rs b/impulse-backends/rapier/src/main/rust/src/query_exports.rs similarity index 85% rename from impulse-rapier/src/main/rust/src/query_exports.rs rename to impulse-backends/rapier/src/main/rust/src/query_exports.rs index df49c598..8b7bae3a 100644 --- a/impulse-rapier/src/main/rust/src/query_exports.rs +++ b/impulse-backends/rapier/src/main/rust/src/query_exports.rs @@ -6,7 +6,6 @@ const MAX_RAYCAST_FLOATS: usize = RAYCAST_HIT_FLOATS * MAX_RAYCAST_HITS; const CONTACT_FLOATS: usize = 15; const MAX_CONTACT_POINTS: usize = 16_384; -const MAX_CONTACT_FLOATS: usize = CONTACT_FLOATS * MAX_CONTACT_POINTS; fn append_bounded(values: &mut Vec, record: &[jfloat], max_floats: usize) -> bool { if record.len() > max_floats.saturating_sub(values.len()) { @@ -104,12 +103,35 @@ pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_getCon _class: JClass, space_handle: jlong, ) -> jfloatArray { - let values = catch_jni_default(Vec::new(), || contact_values(space_handle)); + let values = catch_jni_default(Vec::new(), || { + contact_values(space_handle, MAX_CONTACT_POINTS) + }); + float_array_or_null(&env, &values) +} + +#[no_mangle] +pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_getContactsLimitedNative( + env: JNIEnv, + _class: JClass, + space_handle: jlong, + max_contacts: jint, +) -> jfloatArray { + let values = catch_jni_default(Vec::new(), || { + if max_contacts <= 0 { + Vec::new() + } else { + contact_values( + space_handle, + (max_contacts as usize).min(MAX_CONTACT_POINTS), + ) + } + }); float_array_or_null(&env, &values) } -fn contact_values(space_handle: jlong) -> Vec { +fn contact_values(space_handle: jlong, max_contacts: usize) -> Vec { let mut values: Vec = Vec::new(); + let max_floats = CONTACT_FLOATS * max_contacts.min(MAX_CONTACT_POINTS); with_space(space_handle, (), |space| { 'contacts: for pair in space.narrow_phase.contact_pairs() { let Some(body_a_id) = space.collider_to_body_id.get(&pair.collider1) else { @@ -144,7 +166,7 @@ fn contact_values(space_handle: jlong) -> Vec { contact.dist, contact.warmstart_impulse, ], - MAX_CONTACT_FLOATS, + max_floats, ) { break 'contacts; } diff --git a/impulse-rapier/src/main/rust/src/space_exports.rs b/impulse-backends/rapier/src/main/rust/src/space_exports.rs similarity index 96% rename from impulse-rapier/src/main/rust/src/space_exports.rs rename to impulse-backends/rapier/src/main/rust/src/space_exports.rs index b015f840..4157ad69 100644 --- a/impulse-rapier/src/main/rust/src/space_exports.rs +++ b/impulse-backends/rapier/src/main/rust/src/space_exports.rs @@ -32,16 +32,18 @@ pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_destro #[no_mangle] pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_setGravityNative( - _env: JNIEnv, + mut env: JNIEnv, _class: JClass, space_handle: jlong, x: jfloat, y: jfloat, z: jfloat, ) { - with_space(space_handle, (), |space| { + if let Err(failure) = with_space_checked(space_handle, |space| { space.gravity = finite_vector_or_zero(x, y, z); - }); + }) { + throw_native_space_failure(&mut env, "set gravity", failure); + } } #[no_mangle] @@ -444,7 +446,7 @@ fn long_array_or_null(env: &JNIEnv<'_>, values: &[jlong]) -> jni::sys::jlongArra #[no_mangle] pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_setSolverTuningNative( - _env: JNIEnv, + mut env: JNIEnv, _class: JClass, space_handle: jlong, solver_iterations: jint, @@ -452,28 +454,32 @@ pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_setSol stabilization_iterations: jint, min_island_size: jint, ) { - with_space(space_handle, (), |space| { + if let Err(failure) = with_space_checked(space_handle, |space| { space.set_solver_tuning( positive_usize(solver_iterations), positive_usize(internal_pgs_iterations), non_negative_usize(stabilization_iterations), positive_usize(min_island_size), ); - }); + }) { + throw_native_space_failure(&mut env, "set solver tuning", failure); + } } #[no_mangle] pub extern "system" fn Java_dev_hytalemodding_impulse_rapier_RapierNative_setDynamicSleepTuningNative( - _env: JNIEnv, + mut env: JNIEnv, _class: JClass, space_handle: jlong, linear_threshold: jfloat, angular_threshold: jfloat, time_until_sleep: jfloat, ) { - with_space(space_handle, (), |space| { + if let Err(failure) = with_space_checked(space_handle, |space| { space.set_dynamic_sleep_tuning(linear_threshold, angular_threshold, time_until_sleep); - }); + }) { + throw_native_space_failure(&mut env, "set dynamic sleep tuning", failure); + } } fn positive_usize(value: jint) -> usize { diff --git a/impulse-rapier/src/main/rust/src/voxel_exports.rs b/impulse-backends/rapier/src/main/rust/src/voxel_exports.rs similarity index 100% rename from impulse-rapier/src/main/rust/src/voxel_exports.rs rename to impulse-backends/rapier/src/main/rust/src/voxel_exports.rs diff --git a/impulse-backends/rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierBackendRuntimeProviderTest.java b/impulse-backends/rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierBackendRuntimeProviderTest.java new file mode 100644 index 00000000..ee75685a --- /dev/null +++ b/impulse-backends/rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierBackendRuntimeProviderTest.java @@ -0,0 +1,494 @@ +package dev.hytalemodding.impulse.rapier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendBodySnapshotSink; +import dev.hytalemodding.impulse.api.runtime.BackendJointType; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import java.lang.reflect.Field; +import java.util.Map; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +class RapierBackendRuntimeProviderTest { + + @Test + void providerCreatesIdOnlyRuntime() { + RapierBackendRuntimeProvider provider = new RapierBackendRuntimeProvider(); + provider.init(); + + PhysicsBackendRuntime runtime = provider.createRuntime(); + + assertInstanceOf(RapierBackendRuntime.class, runtime); + } + + @Test + void runtimeSupportsPrimitiveBodySnapshotAndJointLifecycle() { + RapierBackendRuntimeProvider provider = new RapierBackendRuntimeProvider(); + provider.init(); + PhysicsBackendRuntime runtime = provider.createRuntime(); + int spaceId = runtime.createSpace(new SpaceId(70)); + try { + runtime.setGravity(spaceId, 0.0f, -9.81f, 0.0f); + float[] gravity = new float[3]; + runtime.getGravity(spaceId, (x, y, z) -> { + gravity[0] = x; + gravity[1] = y; + gravity[2] = z; + }); + + long firstBodyId = runtime.createBody(spaceId, + BackendRuntimeCodes.shapeTypeCode(ShapeType.BOX), + 0.5f, + 0.5f, + 0.5f, + -1.0f, + -1.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 1.0f, + BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC), + 1.0f, + 2.0f, + 3.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + long secondBodyId = runtime.createBody(spaceId, + BackendRuntimeCodes.shapeTypeCode(ShapeType.BOX), + 0.5f, + 0.5f, + 0.5f, + -1.0f, + -1.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 1.0f, + BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC), + 2.0f, + 2.0f, + 3.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + CapturedSnapshot snapshot = new CapturedSnapshot(); + long jointId = runtime.createJoint(spaceId, + BackendRuntimeCodes.jointTypeCode(BackendJointType.FIXED), + firstBodyId, + secondBodyId, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + false, + 0.0f, + 0.0f); + + assertEquals(-9.81f, gravity[1]); + assertEquals(2, runtime.bodyCount(spaceId)); + assertTrue(runtime.containsBody(spaceId, firstBodyId)); + assertTrue(runtime.bodySnapshot(spaceId, firstBodyId, snapshot)); + assertEquals(firstBodyId, snapshot.bodyId); + assertEquals(BackendRuntimeCodes.shapeTypeCode(ShapeType.BOX), snapshot.shapeTypeCode); + assertEquals(BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC), + snapshot.bodyTypeCode); + assertEquals(1.0f, snapshot.positionX); + assertEquals(2.0f, snapshot.positionY); + assertEquals(3.0f, snapshot.positionZ); + assertEquals(1, runtime.jointCount(spaceId)); + assertEquals(BackendRuntimeCodes.jointTypeCode(BackendJointType.FIXED), + runtime.jointType(spaceId, jointId)); + assertEquals(firstBodyId, runtime.jointBodyA(spaceId, jointId)); + assertEquals(secondBodyId, runtime.jointBodyB(spaceId, jointId)); + + runtime.removeJoint(spaceId, jointId); + runtime.removeBody(spaceId, firstBodyId); + + assertEquals(0, runtime.jointCount(spaceId)); + assertFalse(runtime.containsBody(spaceId, firstBodyId)); + assertEquals(1, runtime.bodyCount(spaceId)); + } finally { + runtime.destroySpace(spaceId); + } + } + + @Test + void gravityRoundTripsEveryAxisDirection() { + RapierBackendRuntimeProvider provider = new RapierBackendRuntimeProvider(); + provider.init(); + PhysicsBackendRuntime runtime = provider.createRuntime(); + float[][] gravities = { + {0.0f, -9.81f, 0.0f}, + {0.0f, 9.81f, 0.0f}, + {9.81f, 0.0f, 0.0f}, + {-9.81f, 0.0f, 0.0f} + }; + for (int index = 0; index < gravities.length; index++) { + int spaceId = runtime.createSpace(new SpaceId(700 + index)); + try { + float[] expected = gravities[index]; + runtime.setGravity(spaceId, expected[0], expected[1], expected[2]); + + assertGravityEquals(expected, runtime, spaceId); + runtime.step(spaceId, 1.0f / 60.0f); + assertGravityEquals(expected, runtime, spaceId); + } finally { + runtime.destroySpace(spaceId); + } + } + } + + @Test + void configuredBodyCreationSeedsInitialBodyState() { + RapierBackendRuntimeProvider provider = new RapierBackendRuntimeProvider(); + provider.init(); + PhysicsBackendRuntime runtime = provider.createRuntime(); + int spaceId = runtime.createSpace(new SpaceId(76)); + try { + long bodyId = runtime.createBodyWithInitialState(spaceId, + BackendRuntimeCodes.shapeTypeCode(ShapeType.BOX), + 0.5f, + 0.5f, + 0.5f, + -1.0f, + -1.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 1.0f, + BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC), + 1.0f, + 2.0f, + 3.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.2f, + 0.3f, + 0.65f, + 0.15f, + 2, + 3, + true, + true); + CapturedSnapshot snapshot = new CapturedSnapshot(); + + assertTrue(runtime.bodySnapshot(spaceId, bodyId, snapshot)); + + assertEquals(0.2f, snapshot.linearDamping); + assertEquals(0.3f, snapshot.angularDamping); + assertEquals(0.65f, snapshot.friction); + assertEquals(0.15f, snapshot.restitution); + assertEquals(2, snapshot.collisionGroup); + assertEquals(3, snapshot.collisionMask); + assertTrue(snapshot.sensor); + assertTrue(snapshot.continuousCollisionEnabled); + } finally { + runtime.destroySpace(spaceId); + } + } + + private static void assertGravityEquals(float[] expected, + PhysicsBackendRuntime runtime, + int spaceId) { + float[] actual = new float[3]; + runtime.getGravity(spaceId, (x, y, z) -> { + actual[0] = x; + actual[1] = y; + actual[2] = z; + }); + assertEquals(expected[0], actual[0], 0.0001f); + assertEquals(expected[1], actual[1], 0.0001f); + assertEquals(expected[2], actual[2], 0.0001f); + } + + @Test + void failedNativeBodyRemovalKeepsJavaBodyStateForRetry() { + RapierBackendRuntimeProvider provider = new RapierBackendRuntimeProvider(); + provider.init(); + PhysicsBackendRuntime runtime = provider.createRuntime(); + int spaceId = runtime.createSpace(new SpaceId(71)); + long bodyId = createBox(runtime, spaceId, 1.0f, 2.0f, 3.0f); + long nativeSpaceHandle = nativeSpaceHandle(runtime, spaceId); + RapierNative.destroySpaceNative(nativeSpaceHandle); + try { + assertThrows(IllegalStateException.class, () -> runtime.removeBody(spaceId, bodyId)); + + assertTrue(runtime.containsBody(spaceId, bodyId)); + assertEquals(1, runtime.bodyCount(spaceId)); + } finally { + runtime.destroySpace(spaceId); + } + } + + @Test + void failedNativeJointRemovalKeepsJavaJointStateForRetry() { + RapierBackendRuntimeProvider provider = new RapierBackendRuntimeProvider(); + provider.init(); + PhysicsBackendRuntime runtime = provider.createRuntime(); + int spaceId = runtime.createSpace(new SpaceId(72)); + long firstBodyId = createBox(runtime, spaceId, 1.0f, 2.0f, 3.0f); + long secondBodyId = createBox(runtime, spaceId, 2.0f, 2.0f, 3.0f); + long jointId = runtime.createJoint(spaceId, + BackendRuntimeCodes.jointTypeCode(BackendJointType.FIXED), + firstBodyId, + secondBodyId, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + false, + 0.0f, + 0.0f); + long nativeSpaceHandle = nativeSpaceHandle(runtime, spaceId); + RapierNative.destroySpaceNative(nativeSpaceHandle); + try { + assertThrows(IllegalStateException.class, () -> runtime.removeJoint(spaceId, jointId)); + + assertEquals(1, runtime.jointCount(spaceId)); + assertEquals(BackendRuntimeCodes.jointTypeCode(BackendJointType.FIXED), + runtime.jointType(spaceId, jointId)); + } finally { + runtime.destroySpace(spaceId); + } + } + + @Test + void failedNativeBodyMutationDoesNotAdvanceCachedState() { + RapierBackendRuntimeProvider provider = new RapierBackendRuntimeProvider(); + provider.init(); + PhysicsBackendRuntime runtime = provider.createRuntime(); + int spaceId = runtime.createSpace(new SpaceId(73)); + long bodyId = createBox(runtime, spaceId, 1.0f, 2.0f, 3.0f); + long nativeSpaceHandle = nativeSpaceHandle(runtime, spaceId); + RapierNative.destroySpaceNative(nativeSpaceHandle); + try { + assertThrows(IllegalStateException.class, + () -> runtime.setBodyPosition(spaceId, bodyId, 9.0f, 9.0f, 9.0f)); + + assertEquals(1.0f, cachedPositionX(runtime, spaceId, bodyId)); + assertEquals(2.0f, cachedPositionY(runtime, spaceId, bodyId)); + assertEquals(3.0f, cachedPositionZ(runtime, spaceId, bodyId)); + } finally { + runtime.destroySpace(spaceId); + } + } + + @Test + void closeDestroysAllCachedSpaces() { + RapierBackendRuntimeProvider provider = new RapierBackendRuntimeProvider(); + provider.init(); + PhysicsBackendRuntime runtime = provider.createRuntime(); + int firstSpaceId = runtime.createSpace(new SpaceId(74)); + int secondSpaceId = runtime.createSpace(new SpaceId(75)); + createBox(runtime, firstSpaceId, 1.0f, 2.0f, 3.0f); + createBox(runtime, secondSpaceId, 2.0f, 2.0f, 3.0f); + + runtime.close(); + + assertThrows(IllegalArgumentException.class, () -> runtime.bodyCount(firstSpaceId)); + assertThrows(IllegalArgumentException.class, () -> runtime.bodyCount(secondSpaceId)); + } + + private static long createBox(@Nonnull PhysicsBackendRuntime runtime, + int spaceId, + float positionX, + float positionY, + float positionZ) { + return runtime.createBody(spaceId, + BackendRuntimeCodes.shapeTypeCode(ShapeType.BOX), + 0.5f, + 0.5f, + 0.5f, + -1.0f, + -1.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 1.0f, + BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC), + positionX, + positionY, + positionZ, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + } + + private static long nativeSpaceHandle(@Nonnull PhysicsBackendRuntime runtime, int spaceId) { + Object state = spaceState(runtime, spaceId); + try { + Field handle = state.getClass().getDeclaredField("nativeSpaceHandle"); + handle.setAccessible(true); + return handle.getLong(state); + } catch (ReflectiveOperationException exception) { + throw new AssertionError("Unable to inspect Rapier native space handle", exception); + } + } + + private static float cachedPositionX(@Nonnull PhysicsBackendRuntime runtime, + int spaceId, + long bodyId) { + return cachedFloat(runtime, spaceId, bodyId, "positionX"); + } + + private static float cachedPositionY(@Nonnull PhysicsBackendRuntime runtime, + int spaceId, + long bodyId) { + return cachedFloat(runtime, spaceId, bodyId, "positionY"); + } + + private static float cachedPositionZ(@Nonnull PhysicsBackendRuntime runtime, + int spaceId, + long bodyId) { + return cachedFloat(runtime, spaceId, bodyId, "positionZ"); + } + + private static float cachedFloat(@Nonnull PhysicsBackendRuntime runtime, + int spaceId, + long bodyId, + @Nonnull String fieldName) { + Object body = bodyState(runtime, spaceId, bodyId); + try { + Field field = body.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.getFloat(body); + } catch (ReflectiveOperationException exception) { + throw new AssertionError("Unable to inspect Rapier body cache", exception); + } + } + + private static Object bodyState(@Nonnull PhysicsBackendRuntime runtime, + int spaceId, + long bodyId) { + Object state = spaceState(runtime, spaceId); + try { + Field bodies = state.getClass().getDeclaredField("bodiesById"); + bodies.setAccessible(true); + @SuppressWarnings("unchecked") + Map bodiesById = (Map) bodies.get(state); + Object body = bodiesById.get(bodyId); + if (body == null) { + throw new AssertionError("No cached body " + bodyId); + } + return body; + } catch (ReflectiveOperationException exception) { + throw new AssertionError("Unable to inspect Rapier body cache", exception); + } + } + + private static Object spaceState(@Nonnull PhysicsBackendRuntime runtime, int spaceId) { + try { + Field spaces = runtime.getClass().getDeclaredField("spaces"); + spaces.setAccessible(true); + @SuppressWarnings("unchecked") + Map spacesById = (Map) spaces.get(runtime); + Object state = spacesById.get(spaceId); + if (state == null) { + throw new AssertionError("No cached space " + spaceId); + } + return state; + } catch (ReflectiveOperationException exception) { + throw new AssertionError("Unable to inspect Rapier runtime cache", exception); + } + } + + private static final class CapturedSnapshot implements BackendBodySnapshotSink { + + private long bodyId; + private int shapeTypeCode; + private int bodyTypeCode; + private float positionX; + private float positionY; + private float positionZ; + private boolean sensor; + private float friction; + private float restitution; + private float linearDamping; + private float angularDamping; + private int collisionGroup; + private int collisionMask; + private boolean continuousCollisionEnabled; + + @Override + public void accept(long bodyId, + int shapeTypeCode, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW, + float linearVelocityX, + float linearVelocityY, + float linearVelocityZ, + float angularVelocityX, + float angularVelocityY, + float angularVelocityZ, + boolean sleeping, + boolean sensor, + float mass, + float friction, + float restitution, + float linearDamping, + float angularDamping, + int collisionGroup, + int collisionMask, + boolean continuousCollisionEnabled, + float centerOfMassOffsetY, + boolean hasBoxHalfExtents, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode) { + this.bodyId = bodyId; + this.shapeTypeCode = shapeTypeCode; + this.bodyTypeCode = bodyTypeCode; + this.positionX = positionX; + this.positionY = positionY; + this.positionZ = positionZ; + this.sensor = sensor; + this.friction = friction; + this.restitution = restitution; + this.linearDamping = linearDamping; + this.angularDamping = angularDamping; + this.collisionGroup = collisionGroup; + this.collisionMask = collisionMask; + this.continuousCollisionEnabled = continuousCollisionEnabled; + } + } +} diff --git a/impulse-backends/rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierBodyDynamicsTest.java b/impulse-backends/rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierBodyDynamicsTest.java new file mode 100644 index 00000000..eeb3f6b8 --- /dev/null +++ b/impulse-backends/rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierBodyDynamicsTest.java @@ -0,0 +1,250 @@ +package dev.hytalemodding.impulse.rapier; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendBodySnapshotSink; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class RapierBodyDynamicsTest { + + @Test + void dynamicBodyFallsUnderGravity() { + PhysicsBackendRuntime runtime = runtime(); + int spaceId = runtime.createSpace(new SpaceId(800)); + try { + runtime.setGravity(spaceId, 0.0f, -9.81f, 0.0f); + long bodyId = createBox(runtime, + spaceId, + 1.0f, + PhysicsBodyType.DYNAMIC, + 0.0f, + 20.0f, + 0.0f); + + step(runtime, spaceId, 60); + + CapturedSnapshot snapshot = requireSnapshot(runtime, spaceId, bodyId); + assertTrue(snapshot.position.y < 20.0f); + assertTrue(snapshot.linearVelocity.y < 0.0f); + } finally { + runtime.destroySpace(spaceId); + } + } + + @Test + void staticBodyDoesNotMoveUnderGravity() { + PhysicsBackendRuntime runtime = runtime(); + int spaceId = runtime.createSpace(new SpaceId(801)); + try { + runtime.setGravity(spaceId, 0.0f, -9.81f, 0.0f); + long bodyId = createBox(runtime, + spaceId, + 0.0f, + PhysicsBodyType.STATIC, + 0.0f, + 10.0f, + 0.0f); + + step(runtime, spaceId, 60); + + CapturedSnapshot snapshot = requireSnapshot(runtime, spaceId, bodyId); + assertEquals(10.0f, snapshot.position.y, 0.01f); + } finally { + runtime.destroySpace(spaceId); + } + } + + @Test + void dynamicBodyLandsOnGroundPlane() { + PhysicsBackendRuntime runtime = runtime(); + int spaceId = runtime.createSpace(new SpaceId(802)); + try { + runtime.setGravity(spaceId, 0.0f, -9.81f, 0.0f); + createPlane(runtime, spaceId, 0.0f); + long bodyId = createBox(runtime, + spaceId, + 1.0f, + PhysicsBodyType.DYNAMIC, + 0.0f, + 5.0f, + 0.0f); + + step(runtime, spaceId, 300); + + CapturedSnapshot snapshot = requireSnapshot(runtime, spaceId, bodyId); + assertTrue(snapshot.position.y < 3.0f); + assertTrue(snapshot.position.y > -0.5f); + } finally { + runtime.destroySpace(spaceId); + } + } + + @Test + void dynamicBodySettlesOnGroundPlane() { + PhysicsBackendRuntime runtime = runtime(); + int spaceId = runtime.createSpace(new SpaceId(803)); + try { + runtime.setGravity(spaceId, 0.0f, -9.81f, 0.0f); + createPlane(runtime, spaceId, 0.0f); + long bodyId = createBox(runtime, + spaceId, + 1.0f, + PhysicsBodyType.DYNAMIC, + 0.0f, + 2.0f, + 0.0f); + + step(runtime, spaceId, 300); + + CapturedSnapshot snapshot = requireSnapshot(runtime, spaceId, bodyId); + assertTrue(snapshot.linearVelocity.length() < 0.5f); + } finally { + runtime.destroySpace(spaceId); + } + } + + @Test + void raycastHitsGroundPlane() { + PhysicsBackendRuntime runtime = runtime(); + int spaceId = runtime.createSpace(new SpaceId(804)); + try { + createPlane(runtime, spaceId, 0.0f); + + int hits = runtime.raycastAll(spaceId, + 0.0f, + 10.0f, + 0.0f, + 0.0f, + -10.0f, + 0.0f, + (_, _, _, _, _, _, _, _, _) -> { + }); + + assertTrue(hits > 0); + } finally { + runtime.destroySpace(spaceId); + } + } + + private static PhysicsBackendRuntime runtime() { + RapierBackendRuntimeProvider provider = new RapierBackendRuntimeProvider(); + provider.init(); + return provider.createRuntime(); + } + + private static void step(PhysicsBackendRuntime runtime, int spaceId, int steps) { + for (int i = 0; i < steps; i++) { + runtime.step(spaceId, 1.0f / 60.0f); + } + } + + private static CapturedSnapshot requireSnapshot(PhysicsBackendRuntime runtime, + int spaceId, + long bodyId) { + CapturedSnapshot snapshot = new CapturedSnapshot(); + if (!runtime.bodySnapshot(spaceId, bodyId, snapshot)) { + throw new AssertionError("Missing backend snapshot for body " + bodyId); + } + return snapshot; + } + + private static long createBox(PhysicsBackendRuntime runtime, + int spaceId, + float mass, + PhysicsBodyType bodyType, + float positionX, + float positionY, + float positionZ) { + return runtime.createBody(spaceId, + BackendRuntimeCodes.shapeTypeCode(ShapeType.BOX), + 0.5f, + 0.5f, + 0.5f, + -1.0f, + -1.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + mass, + BackendRuntimeCodes.bodyTypeCode(bodyType), + positionX, + positionY, + positionZ, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + } + + private static long createPlane(PhysicsBackendRuntime runtime, int spaceId, float groundY) { + return runtime.createBody(spaceId, + BackendRuntimeCodes.shapeTypeCode(ShapeType.PLANE), + -1.0f, + -1.0f, + -1.0f, + -1.0f, + -1.0f, + BackendRuntimeCodes.AXIS_Y, + groundY, + 0.0f, + BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.STATIC), + 0.0f, + groundY, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + } + + private static final class CapturedSnapshot implements BackendBodySnapshotSink { + + private final Vector3f position = new Vector3f(); + private final Vector3f linearVelocity = new Vector3f(); + + @Override + public void accept(long bodyId, + int shapeTypeCode, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW, + float linearVelocityX, + float linearVelocityY, + float linearVelocityZ, + float angularVelocityX, + float angularVelocityY, + float angularVelocityZ, + boolean sleeping, + boolean sensor, + float mass, + float friction, + float restitution, + float linearDamping, + float angularDamping, + int collisionGroup, + int collisionMask, + boolean continuousCollisionEnabled, + float centerOfMassOffsetY, + boolean hasBoxHalfExtents, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode) { + position.set(positionX, positionY, positionZ); + linearVelocity.set(linearVelocityX, linearVelocityY, linearVelocityZ); + } + } +} diff --git a/impulse-backends/rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierBoundedContactsTest.java b/impulse-backends/rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierBoundedContactsTest.java new file mode 100644 index 00000000..5ac00476 --- /dev/null +++ b/impulse-backends/rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierBoundedContactsTest.java @@ -0,0 +1,118 @@ +package dev.hytalemodding.impulse.rapier; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendContactSink; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import org.junit.jupiter.api.Test; + +class RapierBoundedContactsTest { + + @Test + void getContactsWithLimitReturnsAtMostRequestedContacts() { + RapierBackendRuntimeProvider provider = new RapierBackendRuntimeProvider(); + provider.init(); + PhysicsBackendRuntime runtime = provider.createRuntime(); + int spaceId = runtime.createSpace(new SpaceId(1)); + try { + runtime.setGravity(spaceId, 0.0f, -9.81f, 0.0f); + addStaticFloor(runtime, spaceId, 8, 8); + addRestingBoxes(runtime, spaceId, 8, 8); + for (int i = 0; i < 180; i++) { + runtime.step(spaceId, 1.0f / 30.0f); + } + + int contacts = runtime.contacts(spaceId, 5, new CountingContactSink()); + + assertFalse(contacts == 0); + assertTrue(contacts <= 5); + } finally { + runtime.destroySpace(spaceId); + } + } + + private static void addStaticFloor(PhysicsBackendRuntime runtime, + int spaceId, + int width, + int depth) { + for (int x = 0; x < width; x++) { + for (int z = 0; z < depth; z++) { + createBox(runtime, + spaceId, + x, + 0.0f, + z, + 0.5f, + BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.STATIC)); + } + } + } + + private static void addRestingBoxes(PhysicsBackendRuntime runtime, + int spaceId, + int width, + int depth) { + for (int x = 0; x < width; x++) { + for (int z = 0; z < depth; z++) { + createBox(runtime, + spaceId, + x, + 1.05f, + z, + 1.0f, + BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC)); + } + } + } + + private static void createBox(PhysicsBackendRuntime runtime, + int spaceId, + float x, + float y, + float z, + float mass, + int bodyTypeCode) { + runtime.createBody(spaceId, + BackendRuntimeCodes.shapeTypeCode(ShapeType.BOX), + 0.5f, + 0.5f, + 0.5f, + -1.0f, + -1.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + mass, + bodyTypeCode, + x, + y, + z, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + } + + private static final class CountingContactSink implements BackendContactSink { + + @Override + public void accept(long bodyAId, + long bodyBId, + float pointAX, + float pointAY, + float pointAZ, + float pointBX, + float pointBY, + float pointBZ, + float normalBX, + float normalBY, + float normalBZ, + float distance, + float impulse) { + } + } +} diff --git a/impulse-backends/rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierNativeBodyRemovalTest.java b/impulse-backends/rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierNativeBodyRemovalTest.java new file mode 100644 index 00000000..95e8179e --- /dev/null +++ b/impulse-backends/rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierNativeBodyRemovalTest.java @@ -0,0 +1,140 @@ +package dev.hytalemodding.impulse.rapier; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendJointType; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import java.lang.reflect.Field; +import java.util.Map; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +class RapierNativeBodyRemovalTest { + + @Test + void nativeBodyRemovalDropsAttachedJointHandles() { + RapierBackendRuntimeProvider provider = new RapierBackendRuntimeProvider(); + provider.init(); + PhysicsBackendRuntime runtime = provider.createRuntime(); + int spaceId = runtime.createSpace(new SpaceId(4)); + try { + long firstBodyId = createBox(runtime, spaceId); + long secondBodyId = createBox(runtime, spaceId); + runtime.createJoint(spaceId, + BackendRuntimeCodes.jointTypeCode(BackendJointType.FIXED), + firstBodyId, + secondBodyId, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + false, + 0.0f, + 0.0f); + long nativeSpaceHandle = nativeSpaceHandle(runtime, spaceId); + long nativeBodyHandle = nativeBodyHandle(runtime, spaceId, firstBodyId); + + assertEquals(1, RapierNative.jointHandleCountNative(nativeSpaceHandle)); + + RapierNative.removeBodyNative(nativeSpaceHandle, nativeBodyHandle); + + assertEquals(0, RapierNative.jointHandleCountNative(nativeSpaceHandle)); + } finally { + runtime.destroySpace(spaceId); + } + } + + private static long createBox(@Nonnull PhysicsBackendRuntime runtime, int spaceId) { + return runtime.createBody(spaceId, + BackendRuntimeCodes.shapeTypeCode(ShapeType.BOX), + 0.5f, + 0.5f, + 0.5f, + -1.0f, + -1.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 1.0f, + BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC), + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + } + + private static long nativeSpaceHandle(@Nonnull PhysicsBackendRuntime runtime, int spaceId) { + Object state = spaceState(runtime, spaceId); + try { + Field handle = state.getClass().getDeclaredField("nativeSpaceHandle"); + handle.setAccessible(true); + return handle.getLong(state); + } catch (ReflectiveOperationException exception) { + throw new AssertionError("Unable to inspect Rapier native space handle", exception); + } + } + + private static long nativeBodyHandle(@Nonnull PhysicsBackendRuntime runtime, + int spaceId, + long bodyId) { + Object body = bodyState(runtime, spaceId, bodyId); + try { + Field handle = body.getClass().getDeclaredField("nativeBodyHandle"); + handle.setAccessible(true); + return handle.getLong(body); + } catch (ReflectiveOperationException exception) { + throw new AssertionError("Unable to inspect Rapier native body handle", exception); + } + } + + private static Object bodyState(@Nonnull PhysicsBackendRuntime runtime, + int spaceId, + long bodyId) { + Object state = spaceState(runtime, spaceId); + try { + Field bodies = state.getClass().getDeclaredField("bodiesById"); + bodies.setAccessible(true); + @SuppressWarnings("unchecked") + Map bodiesById = (Map) bodies.get(state); + Object body = bodiesById.get(bodyId); + if (body == null) { + throw new AssertionError("No cached body " + bodyId); + } + return body; + } catch (ReflectiveOperationException exception) { + throw new AssertionError("Unable to inspect Rapier body cache", exception); + } + } + + private static Object spaceState(@Nonnull PhysicsBackendRuntime runtime, int spaceId) { + try { + Field spaces = runtime.getClass().getDeclaredField("spaces"); + spaces.setAccessible(true); + @SuppressWarnings("unchecked") + Map spacesById = (Map) spaces.get(runtime); + Object state = spacesById.get(spaceId); + if (state == null) { + throw new AssertionError("No cached space " + spaceId); + } + return state; + } catch (ReflectiveOperationException exception) { + throw new AssertionError("Unable to inspect Rapier runtime cache", exception); + } + } +} diff --git a/impulse-backends/rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierVoxelTerrainTest.java b/impulse-backends/rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierVoxelTerrainTest.java new file mode 100644 index 00000000..ed00eb1c --- /dev/null +++ b/impulse-backends/rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierVoxelTerrainTest.java @@ -0,0 +1,224 @@ +package dev.hytalemodding.impulse.rapier; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendBodySnapshotSink; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class RapierVoxelTerrainTest { + + @Test + void dynamicBoxRestsOnNativeVoxelFloor() { + PhysicsBackendRuntime runtime = runtime(); + int spaceId = runtime.createSpace(new SpaceId(10)); + try { + runtime.setGravity(spaceId, 0.0f, -9.81f, 0.0f); + addVoxelFloor(runtime, spaceId, 0.0f); + long boxId = addDynamicBox(runtime, spaceId, 8.0f, 3.0f, 8.0f); + + StepResult result = stepAndTrackMinimumY(runtime, spaceId, boxId, 240); + + assertFalse(result.fellThrough()); + } finally { + runtime.destroySpace(spaceId); + } + } + + @Test + void dynamicBoxRestsNearNativeVoxelSectionEdge() { + PhysicsBackendRuntime runtime = runtime(); + int spaceId = runtime.createSpace(new SpaceId(11)); + try { + runtime.setGravity(spaceId, 0.0f, -9.81f, 0.0f); + addVoxelFloor(runtime, spaceId, 0.0f); + long boxId = addDynamicBox(runtime, spaceId, 15.75f, 3.0f, 8.0f); + + StepResult result = stepAndTrackMinimumY(runtime, spaceId, boxId, 240); + + assertFalse(result.fellThrough()); + } finally { + runtime.destroySpace(spaceId); + } + } + + @Test + void dynamicBoxRestsAcrossStitchedNativeVoxelSections() { + PhysicsBackendRuntime runtime = runtime(); + int spaceId = runtime.createSpace(new SpaceId(12)); + try { + runtime.setGravity(spaceId, 0.0f, -9.81f, 0.0f); + long first = addVoxelFloor(runtime, spaceId, 0.0f); + long second = addVoxelFloor(runtime, spaceId, 16.0f); + runtime.combineVoxelTerrains(spaceId, first, second, 16, 0, 0); + long boxId = addDynamicBox(runtime, spaceId, 16.0f, 3.0f, 8.0f); + + StepResult result = stepAndTrackMinimumY(runtime, spaceId, boxId, 240); + + assertFalse(result.fellThrough()); + } finally { + runtime.destroySpace(spaceId); + } + } + + @Test + void combineVoxelTerrainNativeReportsInvalidSpaceHandle() { + RapierNative.load(); + + assertFalse(RapierNative.combineVoxelTerrainNative(0L, 1L, 2L, 1, 0, 0)); + } + + @Test + void combineVoxelTerrainsRejectsNonVoxelBodies() { + PhysicsBackendRuntime runtime = runtime(); + int spaceId = runtime.createSpace(new SpaceId(2)); + try { + long voxelBody = addVoxelFloor(runtime, spaceId, 0.0f); + long box = addDynamicBox(runtime, spaceId, 0.0f, 1.0f, 0.0f); + + assertThrows(IllegalArgumentException.class, + () -> runtime.combineVoxelTerrains(spaceId, voxelBody, box, 1, 0, 0)); + } finally { + runtime.destroySpace(spaceId); + } + } + + @Test + void combineVoxelTerrainsRejectsSameBody() { + PhysicsBackendRuntime runtime = runtime(); + int spaceId = runtime.createSpace(new SpaceId(3)); + try { + long voxelBody = addVoxelFloor(runtime, spaceId, 0.0f); + + assertThrows(IllegalArgumentException.class, + () -> runtime.combineVoxelTerrains(spaceId, voxelBody, voxelBody, 1, 0, 0)); + } finally { + runtime.destroySpace(spaceId); + } + } + + private static PhysicsBackendRuntime runtime() { + RapierBackendRuntimeProvider provider = new RapierBackendRuntimeProvider(); + provider.init(); + return provider.createRuntime(); + } + + private static long addVoxelFloor(PhysicsBackendRuntime runtime, int spaceId, float originX) { + return runtime.createVoxelTerrain(spaceId, + 1.0f, + 1.0f, + 1.0f, + voxelFloorCoordinates(16, 16), + originX, + 0.0f, + 0.0f, + 0.5f, + 0.0f, + 1, + 1); + } + + private static int[] voxelFloorCoordinates(int width, int depth) { + int[] coordinates = new int[width * depth * 3]; + int index = 0; + for (int x = 0; x < width; x++) { + for (int z = 0; z < depth; z++) { + coordinates[index++] = x; + coordinates[index++] = 0; + coordinates[index++] = z; + } + } + return coordinates; + } + + private static long addDynamicBox(PhysicsBackendRuntime runtime, int spaceId, float x, float y, float z) { + return runtime.createBody(spaceId, + BackendRuntimeCodes.shapeTypeCode(ShapeType.BOX), + 0.45f, + 0.45f, + 0.45f, + -1.0f, + -1.0f, + BackendRuntimeCodes.AXIS_Y, + 0.0f, + 1.0f, + BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC), + x, + y, + z, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + } + + private static StepResult stepAndTrackMinimumY(PhysicsBackendRuntime runtime, + int spaceId, + long bodyId, + int steps) { + CapturedSnapshot snapshot = new CapturedSnapshot(); + float minY = Float.POSITIVE_INFINITY; + for (int i = 0; i < steps; i++) { + runtime.step(spaceId, 1.0f / 30.0f); + runtime.bodySnapshot(spaceId, bodyId, snapshot); + minY = Math.min(minY, snapshot.position.y); + } + return new StepResult(new Vector3f(snapshot.position), minY); + } + + private record StepResult(Vector3f finalPosition, float minY) { + + private boolean fellThrough() { + return finalPosition.y < 1.0f || minY < 0.75f; + } + } + + private static final class CapturedSnapshot implements BackendBodySnapshotSink { + + private final Vector3f position = new Vector3f(); + + @Override + public void accept(long bodyId, + int shapeTypeCode, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW, + float linearVelocityX, + float linearVelocityY, + float linearVelocityZ, + float angularVelocityX, + float angularVelocityY, + float angularVelocityZ, + boolean sleeping, + boolean sensor, + float mass, + float friction, + float restitution, + float linearDamping, + float angularDamping, + int collisionGroup, + int collisionMask, + boolean continuousCollisionEnabled, + float centerOfMassOffsetY, + boolean hasBoxHalfExtents, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode) { + position.set(positionX, positionY, positionZ); + } + } +} diff --git a/impulse-builtins/control/build.gradle.kts b/impulse-builtins/control/build.gradle.kts new file mode 100644 index 00000000..93c8cd19 --- /dev/null +++ b/impulse-builtins/control/build.gradle.kts @@ -0,0 +1,54 @@ +import org.gradle.api.tasks.Delete +import org.gradle.api.tasks.SourceSetContainer +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.api.tasks.testing.Test + +plugins { + id("java-library") +} + +version = rootProject.version + +evaluationDependsOn(":impulse-core") + +val coreMain = project(":impulse-core") + .extensions + .getByType() + .named("main") + .get() + +dependencies { + api(project(":impulse-backends:api")) + compileOnly(coreMain.output) + compileOnly(files(coreMain.compileClasspath)) + compileOnly(project(":impulse-early-plugin")) + testImplementation(project(":impulse-core")) + testImplementation(project(":impulse-early-plugin")) + testImplementation(testFixtures(project(":impulse-backends:api"))) + testImplementation(libs.objenesis) + testRuntimeOnly(project(":impulse-early-plugin")) + testCompileOnly(files(coreMain.compileClasspath)) + testRuntimeOnly(files(coreMain.compileClasspath)) + compileOnly(libs.lombok) + annotationProcessor(libs.lombok) +} + +tasks.named("compileJava") { + dependsOn(":impulse-core:compileJava") +} + +tasks.withType().configureEach { + jvmArgs("-Djava.util.logging.manager=com.hypixel.hytale.logger.backend.HytaleLogManager") +} + +val removeStandaloneManifest by tasks.registering(Delete::class) { + delete(layout.buildDirectory.file("resources/main/manifest.json")) +} + +tasks.named("processResources") { + dependsOn(removeStandaloneManifest) +} + +tasks.named("jar") { + exclude("manifest.json") +} diff --git a/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/ImpulseControlPlugin.java b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/ImpulseControlPlugin.java new file mode 100644 index 00000000..07fd4247 --- /dev/null +++ b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/ImpulseControlPlugin.java @@ -0,0 +1,89 @@ +package dev.hytalemodding.impulse.builtin.control; + +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.builtin.control.internal.ControlLifecycle; +import dev.hytalemodding.impulse.builtin.control.internal.ControlTypeRegistry; +import dev.hytalemodding.impulse.builtin.control.internal.PhysicsControlRuntimeStates; +import dev.hytalemodding.impulse.builtin.control.internal.systems.PhysicsControllableLifecycleSystem; +import dev.hytalemodding.impulse.builtin.control.internal.systems.PhysicsControlRuntimeHolderSystem; +import dev.hytalemodding.impulse.builtin.control.internal.systems.PhysicsControlSessionCleanupSystem; +import dev.hytalemodding.impulse.builtin.control.internal.systems.PhysicsKinematicControlSystem; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsCleanupHooks; +import java.util.UUID; +import java.util.function.Consumer; +import javax.annotation.Nonnull; + +/** + * Builtin plugin that enables Impulse kinematic control sessions. + */ +@Deprecated +public final class ImpulseControlPlugin extends JavaPlugin { + + @Nonnull + private static final PhysicsCleanupHooks.EntityStoreCleanup ENTITY_STORE_CLEANUP = + ControlLifecycle::cleanupStoreForExternalCleanup; + @Nonnull + private static final PhysicsCleanupHooks.SelectedEntityStoreCleanup SELECTED_ENTITY_STORE_CLEANUP = + ControlLifecycle::cleanupSelectedStoreForExternalCleanup; + @Nonnull + private static final Consumer> BODY_RUNTIME_CLEANUP = + PhysicsControlRuntimeStates::clear; + @Nonnull + private static final PhysicsCleanupHooks.BodyRowCleanup BODY_ROW_CLEANUP = + ImpulseControlPlugin::clearControlledBody; + + public ImpulseControlPlugin(@Nonnull JavaPluginInit init) { + super(init); + } + + @Override + protected void setup() { + ComponentRegistryProxy entityRegistry = getEntityStoreRegistry(); + ControlTypeRegistry.registerComponentTypes(entityRegistry); + entityRegistry.registerSystem(new PhysicsControlRuntimeHolderSystem()); + entityRegistry.registerSystem(new PhysicsControllableLifecycleSystem()); + entityRegistry.registerSystem(new PhysicsControlSessionCleanupSystem()); + entityRegistry.registerSystem(new PhysicsKinematicControlSystem()); + registerCleanupHooks(); + ControlLifecycle.enable(); + } + + @Override + protected void shutdown() { + ControlLifecycle.disable(); + unregisterCleanupHooks(); + ControlTypeRegistry.clearComponentTypes(); + } + + private static void registerCleanupHooks() { + PhysicsCleanupHooks.registerDetachableEntityMarker( + ImpulseControllableComponent.getComponentType()); + PhysicsCleanupHooks.registerEntityStoreCleanup(ENTITY_STORE_CLEANUP); + PhysicsCleanupHooks.registerSelectedEntityStoreCleanup(SELECTED_ENTITY_STORE_CLEANUP); + PhysicsCleanupHooks.registerBodyRuntimeCleanup(BODY_RUNTIME_CLEANUP); + PhysicsCleanupHooks.registerBodyRowCleanup(BODY_ROW_CLEANUP); + } + + private static void unregisterCleanupHooks() { + if (ImpulseControllableComponent.isComponentTypeRegistered()) { + PhysicsCleanupHooks.unregisterDetachableEntityMarker( + ImpulseControllableComponent.getComponentType()); + } + PhysicsCleanupHooks.unregisterEntityStoreCleanup(ENTITY_STORE_CLEANUP); + PhysicsCleanupHooks.unregisterSelectedEntityStoreCleanup(SELECTED_ENTITY_STORE_CLEANUP); + PhysicsCleanupHooks.unregisterBodyRuntimeCleanup(BODY_RUNTIME_CLEANUP); + PhysicsCleanupHooks.unregisterBodyRowCleanup(BODY_ROW_CLEANUP); + } + + private static void clearControlledBody(@Nonnull Store store, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef) { + PhysicsControlRuntimeStates.clearControlled(bodyRef); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/control/ImpulseControllableComponent.java b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/ImpulseControllableComponent.java similarity index 55% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/control/ImpulseControllableComponent.java rename to impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/ImpulseControllableComponent.java index 3ef79e4a..84b1cf14 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/control/ImpulseControllableComponent.java +++ b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/ImpulseControllableComponent.java @@ -1,12 +1,11 @@ -package dev.hytalemodding.impulse.core.plugin.modules.control; +package dev.hytalemodding.impulse.builtin.control; import com.hypixel.hytale.codec.builder.BuilderCodec; import com.hypixel.hytale.component.Component; import com.hypixel.hytale.component.ComponentType; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import java.util.Objects; +import dev.hytalemodding.impulse.builtin.control.internal.ControlTypeRegistry; import javax.annotation.Nonnull; -import javax.annotation.Nullable; public class ImpulseControllableComponent implements Component { @@ -16,28 +15,13 @@ public class ImpulseControllableComponent implements Component { ImpulseControllableComponent::new) .build(); - @Nullable - private static ComponentType componentType; - - public static void setComponentType( - @Nonnull ComponentType type) { - componentType = Objects.requireNonNull(type, "type"); - } - - public static void clearComponentType() { - componentType = null; - } - public static boolean isComponentTypeRegistered() { - return componentType != null; + return ControlTypeRegistry.isControllableComponentTypeRegistered(); } @Nonnull public static ComponentType getComponentType() { - if (componentType == null) { - throw new IllegalStateException("Impulse controllable component is not registered"); - } - return componentType; + return ControlTypeRegistry.controllableComponentType(); } @Nonnull diff --git a/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/PhysicsControlSessions.java b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/PhysicsControlSessions.java new file mode 100644 index 00000000..0c8d4930 --- /dev/null +++ b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/PhysicsControlSessions.java @@ -0,0 +1,253 @@ +package dev.hytalemodding.impulse.builtin.control; + +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.builtin.control.internal.ControlLifecycle; +import dev.hytalemodding.impulse.builtin.control.internal.PhysicsControlRuntimeStates; +import dev.hytalemodding.impulse.builtin.control.internal.components.PhysicsControlSessionComponent; +import dev.hytalemodding.impulse.builtin.control.internal.systems.PhysicsKinematicControlSystem; +import dev.hytalemodding.impulse.builtin.control.internal.systems.PhysicsStoreControlSessionMutations; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3f; + +/** + * Public stateless helper for Impulse-managed kinematic control sessions. + */ +public final class PhysicsControlSessions { + + private PhysicsControlSessions() { + } + + /** + * Returns whether the control subplugin is loaded and its component types are registered. + */ + public static boolean isAvailable() { + return ControlLifecycle.isEnabled() + && ImpulseControllableComponent.isComponentTypeRegistered() + && PhysicsControlSessionComponent.isComponentTypeRegistered(); + } + + /** + * Returns whether the controller entity currently has an active Impulse control session. + */ + public static boolean hasSession(@Nonnull Store store, + @Nonnull Ref controllerRef) { + if (!isAvailable()) { + return false; + } + PhysicsControlSessionComponent session = + store.getComponent(controllerRef, PhysicsControlSessionComponent.getComponentType()); + return session != null && session.isActive(); + } + + /** + * Returns whether the PhysicsStore body is currently driven by any active Impulse control + * session. + */ + public static boolean isBodyControlled(@Nullable Ref bodyRef) { + if (!ControlLifecycle.isEnabled()) { + return false; + } + return PhysicsControlRuntimeStates.isControlled(bodyRef); + } + + /** + * Returns whether the controller's active session targets the supplied PhysicsStore body. + */ + public static boolean hasSessionForBody(@Nonnull Store store, + @Nonnull Ref controllerRef, + @Nullable Ref bodyRef) { + if (!isAvailable()) { + return false; + } + PhysicsControlSessionComponent session = + store.getComponent(controllerRef, PhysicsControlSessionComponent.getComponentType()); + return session != null + && session.isActive() + && samePhysicsStoreRef(session.getBodyRef(), bodyRef); + } + + /** + * Starts or replaces the controller entity's Impulse control session from durable entity UUIDs. + * Prefer the ref overload when the caller already has live PhysicsStore entity refs. + */ + public static void startSession(@Nonnull Store store, + @Nonnull Ref controllerRef, + @Nonnull UUID bodyUuid, + @Nonnull UUID anchorBodyUuid, + @Nullable UUID controlJointUuid, + @Nullable Ref targetRef, + @Nonnull PhysicsBodyType originalBodyType, + float grabDistance, + @Nonnull Vector3f viewOffset, + @Nonnull Vector3f previousTarget) { + Store physicsStore = physicsStore(store); + PhysicsThreading.requireWorldThread(physicsStore, + "resolve PhysicsStore control-session UUIDs"); + Ref bodyRef = requireRef(physicsStore, bodyUuid, "body"); + Ref anchorBodyRef = requireRef(physicsStore, anchorBodyUuid, "anchor body"); + Ref controlJointRef = controlJointUuid != null + ? requireRef(physicsStore, controlJointUuid, "control joint") + : null; + startSession(store, + controllerRef, + bodyRef, + anchorBodyRef, + controlJointRef, + targetRef, + originalBodyType, + grabDistance, + viewOffset, + previousTarget); + } + + /** + * Starts or replaces the controller entity's Impulse control session with live PhysicsStore + * entity refs. + */ + public static void startSession(@Nonnull Store store, + @Nonnull Ref controllerRef, + @Nonnull Ref bodyRef, + @Nonnull Ref anchorBodyRef, + @Nullable Ref controlJointRef, + @Nullable Ref targetRef, + @Nonnull PhysicsBodyType originalBodyType, + float grabDistance, + @Nonnull Vector3f viewOffset, + @Nonnull Vector3f previousTarget) { + requireAvailable(); + ControlLifecycle.registerStore(store); + validateControlRefs(bodyRef, anchorBodyRef, controlJointRef); + ComponentType sessionType = + PhysicsControlSessionComponent.getComponentType(); + releaseSession(store, controllerRef, sessionType); + if (PhysicsControlRuntimeStates.isControlled(bodyRef)) { + throw new IllegalStateException("PhysicsStore body is already controlled"); + } + store.putComponent(controllerRef, + sessionType, + new PhysicsControlSessionComponent(bodyRef, + anchorBodyRef, + controlJointRef, + targetRef, + originalBodyType, + grabDistance, + viewOffset, + previousTarget)); + PhysicsControlRuntimeStates.markControlled(bodyRef); + } + + /** + * Releases and removes an active Impulse control session. + * + * @return false when no session existed + */ + public static boolean releaseSession(@Nonnull Store store, + @Nonnull Ref controllerRef) { + if (!isAvailable()) { + return false; + } + return releaseSession(store, + controllerRef, + PhysicsControlSessionComponent.getComponentType()); + } + + private static boolean releaseSession(@Nonnull Store store, + @Nonnull Ref controllerRef, + @Nonnull ComponentType sessionType) { + PhysicsControlSessionComponent session = + store.getComponent(controllerRef, sessionType); + if (session == null) { + return false; + } + + releaseSession(store, controllerRef, sessionType, session); + return true; + } + + private static void releaseSession(@Nonnull Store store, + @Nonnull Ref controllerRef, + @Nonnull ComponentType sessionType, + @Nonnull PhysicsControlSessionComponent session) { + Ref bodyRef = session.getBodyRef(); + PhysicsKinematicControlSystem.clearMutationState(store, session.getAnchorBodyRef()); + if (bodyRef != null) { + PhysicsControlRuntimeStates.clearControlled(bodyRef); + } + PhysicsStoreControlSessionMutations.applyRelease(store, session); + + session.deactivate(); + store.removeComponent(controllerRef, sessionType); + } + + @Nonnull + private static Store physicsStore(@Nonnull Store store) { + return PhysicsThreading.store(store.getExternalData().getWorld()); + } + + @Nonnull + private static Ref requireRef(@Nonnull Store store, + @Nonnull UUID uuid, + @Nonnull String role) { + Ref ref = PhysicsEntities.resolveRef(store, uuid); + if (ref == null) { + throw new IllegalArgumentException("PhysicsStore " + role + + " entity is not loaded for uuid=" + uuid); + } + return ref; + } + + private static void validateControlRefs(@Nonnull Ref bodyRef, + @Nonnull Ref anchorBodyRef, + @Nullable Ref controlJointRef) { + Store store = bodyRef.getStore(); + PhysicsThreading.requireWorldThread(store, + "start PhysicsStore control session"); + requireValidRef(bodyRef, "body"); + requireValidRef(anchorBodyRef, "anchor body"); + if (anchorBodyRef.getStore() != store + || (controlJointRef != null && controlJointRef.getStore() != store)) { + throw new IllegalArgumentException("PhysicsStore control-session refs must belong " + + "to the same PhysicsStore"); + } + if (controlJointRef != null) { + requireValidRef(controlJointRef, "control joint"); + } + } + + private static void requireValidRef(@Nonnull Ref ref, + @Nonnull String role) { + if (!ref.isValid()) { + throw new IllegalArgumentException("PhysicsStore control-session " + role + + " ref is not valid"); + } + } + + private static boolean samePhysicsStoreRef(@Nullable Ref first, + @Nullable Ref second) { + return first != null + && second != null + && first.getStore() == second.getStore() + && first.isValid() + && second.isValid() + && first.getIndex() == second.getIndex(); + } + + private static void requireAvailable() { + ControlLifecycle.requireEnabled(); + if (!ImpulseControllableComponent.isComponentTypeRegistered() + || !PhysicsControlSessionComponent.isComponentTypeRegistered()) { + throw new IllegalStateException( + "Impulse control is disabled. Enable HytaleModding:ImpulseControl to start control sessions."); + } + } + +} diff --git a/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/ControlLifecycle.java b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/ControlLifecycle.java new file mode 100644 index 00000000..b2406b88 --- /dev/null +++ b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/ControlLifecycle.java @@ -0,0 +1,316 @@ +package dev.hytalemodding.impulse.builtin.control.internal; + +import com.hypixel.hytale.assetstore.AssetRegistry; +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.SubPluginLifecycleGate; +import dev.hytalemodding.impulse.builtin.control.internal.components.PhysicsControlSessionComponent; +import dev.hytalemodding.impulse.builtin.control.internal.systems.PhysicsControlSessionCleanup; +import dev.hytalemodding.impulse.builtin.control.ImpulseControllableComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.logging.Level; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3d; + +/** + * Server-level lifecycle controlled by the Impulse control subplugin. + */ +public final class ControlLifecycle { + + private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); + private static final SubPluginLifecycleGate GATE = new SubPluginLifecycleGate( + "Impulse control is disabled. Enable HytaleModding:ImpulseControl to start control sessions."); + private static final Set> STORES = + Collections.newSetFromMap(new WeakHashMap<>()); + private static final long CLEANUP_TIMEOUT_SECONDS = 5L; + + static { + GATE.onDisable(ControlLifecycle::cleanupStores); + GATE.onDisable(PhysicsControlRuntimeStates::clearAll); + } + + private ControlLifecycle() { + } + + public static void enable() { + GATE.enable(); + } + + public static void disable() { + GATE.disable(); + } + + public static boolean isEnabled() { + return GATE.isEnabled(); + } + + public static void requireEnabled() { + GATE.requireEnabled(); + } + + public static void registerStore(@Nonnull Store store) { + synchronized (STORES) { + STORES.add(store); + } + } + + private static void cleanupStores() { + ComponentType controllableType = + ImpulseControllableComponent.isComponentTypeRegistered() + ? ImpulseControllableComponent.getComponentType() + : null; + ComponentType sessionType = + PhysicsControlSessionComponent.isComponentTypeRegistered() + ? PhysicsControlSessionComponent.getComponentType() + : null; + if (controllableType == null && sessionType == null) { + return; + } + ArrayList> stores; + synchronized (STORES) { + stores = new ArrayList<>(STORES); + } + for (Store store : stores) { + try { + cleanupStore(store, controllableType, sessionType); + } catch (RuntimeException exception) { + LOGGER.at(Level.WARNING).log("Failed to clean Impulse control components: %s", + exception.getMessage()); + } + } + } + + public static int cleanupStoreForExternalCleanup(@Nonnull Store store) { + ComponentType controllableType = + ImpulseControllableComponent.isComponentTypeRegistered() + ? ImpulseControllableComponent.getComponentType() + : null; + ComponentType sessionType = + PhysicsControlSessionComponent.isComponentTypeRegistered() + ? PhysicsControlSessionComponent.getComponentType() + : null; + if (controllableType == null && sessionType == null) { + return 0; + } + return cleanupStore(store, controllableType, sessionType); + } + + public static int cleanupSelectedStoreForExternalCleanup(@Nonnull Store store, + @Nonnull Set selectedBodyUuids, + @Nonnull Vector3d center, + double radiusSquared) { + if (!PhysicsControlSessionComponent.isComponentTypeRegistered()) { + return 0; + } + return cleanupSelectedStoreOnWorldThread(store, + PhysicsControlSessionComponent.getComponentType(), + selectedBodyUuids, + center, + radiusSquared); + } + + private static int cleanupStore(@Nonnull Store store, + @Nullable ComponentType controllableType, + @Nullable ComponentType sessionType) { + World world = store.getExternalData().getWorld(); + if (world.isInThread()) { + return cleanupStoreOnWorldThread(store, controllableType, sessionType); + } + if (!world.isStarted()) { + return 0; + } + + // PluginManager.unload holds the asset write lock while world ticks drain queued + // tasks under the read lock, so waiting here would stall until the timeout. + boolean waitForCleanup = !isAssetWriteLockHeldByCurrentThread(); + CompletableFuture cleanup = waitForCleanup ? new CompletableFuture<>() : null; + try { + world.execute(() -> cleanupStoreSafely(store, controllableType, sessionType, cleanup)); + } catch (RuntimeException exception) { + if (isWorldTaskRejection(exception)) { + return 0; + } + throw exception; + } + + if (cleanup != null) { + return cleanup.orTimeout(CLEANUP_TIMEOUT_SECONDS, TimeUnit.SECONDS).join(); + } + return 0; + } + + private static boolean isAssetWriteLockHeldByCurrentThread() { + ReadWriteLock lock = AssetRegistry.ASSET_LOCK; + return lock instanceof ReentrantReadWriteLock reentrantLock + && reentrantLock.isWriteLockedByCurrentThread(); + } + + private static void cleanupStoreSafely(@Nonnull Store store, + @Nullable ComponentType controllableType, + @Nullable ComponentType sessionType, + @Nullable CompletableFuture completion) { + try { + int removedSessions = cleanupStoreOnWorldThread(store, controllableType, sessionType); + if (completion != null) { + completion.complete(removedSessions); + } + } catch (RuntimeException exception) { + if (completion != null) { + completion.completeExceptionally(exception); + } else { + LOGGER.at(Level.WARNING).log("Failed to clean Impulse control components: %s", + exception.getMessage()); + } + } + } + + private static boolean isWorldTaskRejection(@Nonnull RuntimeException exception) { + Throwable current = exception; + while (current != null) { + if (current instanceof IllegalThreadStateException) { + return true; + } + current = current.getCause(); + } + return false; + } + + private static int cleanupStoreOnWorldThread(@Nonnull Store store, + @Nullable ComponentType controllableType, + @Nullable ComponentType sessionType) { + int removedSessions = 0; + if (sessionType != null) { + ArrayList sessions = new ArrayList<>(); + store.forEachEntityParallel(sessionType, + (index, archetypeChunk, _) -> { + PhysicsControlSessionComponent session = + archetypeChunk.getComponent(index, sessionType); + if (session != null) { + SessionCleanupTarget target = new SessionCleanupTarget( + archetypeChunk.getReferenceTo(index), + session); + synchronized (sessions) { + sessions.add(target); + } + } + }); + for (SessionCleanupTarget target : sessions) { + PhysicsControlSessionCleanup.cleanup(store, target.session()); + store.removeComponent(target.ref(), sessionType); + } + removedSessions = sessions.size(); + } + if (controllableType != null) { + store.forEachEntityParallel(controllableType, + (index, archetypeChunk, commandBuffer) -> + commandBuffer.removeComponent(archetypeChunk.getReferenceTo(index), + controllableType)); + } + return removedSessions; + } + + private static int cleanupSelectedStoreOnWorldThread(@Nonnull Store store, + @Nonnull ComponentType sessionType, + @Nonnull Set selectedBodyUuids, + @Nonnull Vector3d center, + double radiusSquared) { + ArrayList sessions = new ArrayList<>(); + store.forEachEntityParallel(sessionType, + (index, archetypeChunk, commandBuffer) -> { + PhysicsControlSessionComponent session = + archetypeChunk.getComponent(index, sessionType); + if (session == null || !controlSessionSelected(commandBuffer, + archetypeChunk, + index, + session, + selectedBodyUuids, + center, + radiusSquared)) { + return; + } + SessionCleanupTarget target = new SessionCleanupTarget( + archetypeChunk.getReferenceTo(index), + session); + synchronized (sessions) { + sessions.add(target); + } + }); + for (SessionCleanupTarget target : sessions) { + PhysicsControlSessionCleanup.cleanup(store, target.session()); + store.removeComponent(target.ref(), sessionType); + } + return sessions.size(); + } + + private static boolean controlSessionSelected( + @Nonnull CommandBuffer commandBuffer, + @Nonnull ArchetypeChunk archetypeChunk, + int index, + @Nonnull PhysicsControlSessionComponent session, + @Nonnull Set selectedBodyUuids, + @Nonnull Vector3d center, + double radiusSquared) { + if (containsBody(selectedBodyUuids, session.getBodyRef()) + || containsBody(selectedBodyUuids, session.getAnchorBodyRef()) + || entityWithinRadius(archetypeChunk, index, center, radiusSquared)) { + return true; + } + + Ref targetRef = session.getTargetRef(); + if (targetRef == null || !targetRef.isValid()) { + return false; + } + + TransformComponent targetTransform = + commandBuffer.getComponent(targetRef, TransformComponent.getComponentType()); + return targetTransform != null && targetTransform.getPosition().distanceSquared(center) + <= radiusSquared; + } + + private static boolean containsBody(@Nonnull Set bodyUuids, + @Nullable Ref bodyRef) { + UUID bodyUuid = rowUuid(bodyRef); + return bodyUuid != null && bodyUuids.contains(bodyUuid); + } + + @Nullable + private static UUID rowUuid(@Nullable Ref bodyRef) { + if (bodyRef == null || !bodyRef.isValid()) { + return null; + } + UuidComponent uuid = bodyRef.getStore().getComponent(bodyRef, UuidComponent.getComponentType()); + return uuid != null ? uuid.getUuid() : null; + } + + private static boolean entityWithinRadius(@Nonnull ArchetypeChunk archetypeChunk, + int index, + @Nonnull Vector3d center, + double radiusSquared) { + TransformComponent transform = + archetypeChunk.getComponent(index, TransformComponent.getComponentType()); + return transform != null && transform.getPosition().distanceSquared(center) <= radiusSquared; + } + + private record SessionCleanupTarget( + @Nonnull Ref ref, + @Nonnull PhysicsControlSessionComponent session) { + } +} diff --git a/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/ControlTypeRegistry.java b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/ControlTypeRegistry.java new file mode 100644 index 00000000..cfc725ba --- /dev/null +++ b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/ControlTypeRegistry.java @@ -0,0 +1,62 @@ +package dev.hytalemodding.impulse.builtin.control.internal; + +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.IComponentRegistry; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.builtin.control.internal.components.PhysicsControlSessionComponent; +import dev.hytalemodding.impulse.builtin.control.ImpulseControllableComponent; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Internal registration owner for the ImpulseControl subplugin component types. + */ +public final class ControlTypeRegistry { + + @Nullable + private static ComponentType controllableComponentType; + @Nullable + private static ComponentType sessionComponentType; + + private ControlTypeRegistry() { + } + + public static void registerComponentTypes(@Nonnull IComponentRegistry registry) { + controllableComponentType = registry.registerComponent( + ImpulseControllableComponent.class, + "ImpulseControllable", + ImpulseControllableComponent.CODEC); + sessionComponentType = registry.registerComponent( + PhysicsControlSessionComponent.class, + PhysicsControlSessionComponent::new); + } + + public static void clearComponentTypes() { + controllableComponentType = null; + sessionComponentType = null; + } + + public static boolean isControllableComponentTypeRegistered() { + return controllableComponentType != null; + } + + public static boolean isSessionComponentTypeRegistered() { + return sessionComponentType != null; + } + + @Nonnull + public static ComponentType controllableComponentType() { + if (controllableComponentType == null) { + throw new IllegalStateException("Impulse controllable component is not registered"); + } + return controllableComponentType; + } + + @Nonnull + public static ComponentType sessionComponentType() { + if (sessionComponentType == null) { + throw new IllegalStateException("Physics control session component is not registered"); + } + return sessionComponentType; + } +} diff --git a/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/PhysicsControlRuntimeState.java b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/PhysicsControlRuntimeState.java new file mode 100644 index 00000000..a38f2815 --- /dev/null +++ b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/PhysicsControlRuntimeState.java @@ -0,0 +1,63 @@ +package dev.hytalemodding.impulse.builtin.control.internal; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import javax.annotation.Nonnull; + +/** + * Runtime-only state for bodies currently driven by an Impulse control session. + */ +public final class PhysicsControlRuntimeState { + + private final Int2ObjectOpenHashMap> controlledBodyRefsByRowIndex = + new Int2ObjectOpenHashMap<>(); + + public synchronized void markBodyControlled(@Nonnull Ref bodyRef) { + controlledBodyRefsByRowIndex.put(bodyRef.getIndex(), bodyRef); + } + + public synchronized void clearControlledBody(@Nonnull Ref bodyRef) { + remove(bodyRef); + } + + public synchronized boolean isBodyControlled(@Nonnull Ref bodyRef) { + Ref controlledRef = controlledBodyRefsByRowIndex.get(bodyRef.getIndex()); + if (controlledRef == null) { + return false; + } + if (!controlledRef.isValid()) { + controlledBodyRefsByRowIndex.remove(bodyRef.getIndex()); + return false; + } + return controlledRef == bodyRef || sameLiveRef(controlledRef, bodyRef); + } + + public synchronized void clearBody(@Nonnull Ref bodyRef) { + remove(bodyRef); + } + + public synchronized void clear() { + controlledBodyRefsByRowIndex.clear(); + } + + private void remove(@Nonnull Ref bodyRef) { + Ref controlledRef = controlledBodyRefsByRowIndex.get(bodyRef.getIndex()); + if (controlledRef == null) { + return; + } + if (controlledRef == bodyRef + || !controlledRef.isValid() + || sameLiveRef(controlledRef, bodyRef)) { + controlledBodyRefsByRowIndex.remove(bodyRef.getIndex()); + } + } + + private static boolean sameLiveRef(@Nonnull Ref first, + @Nonnull Ref second) { + return first.isValid() + && second.isValid() + && first.getStore() == second.getStore() + && first.getIndex() == second.getIndex(); + } +} diff --git a/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/PhysicsControlRuntimeStates.java b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/PhysicsControlRuntimeStates.java new file mode 100644 index 00000000..1b31faad --- /dev/null +++ b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/PhysicsControlRuntimeStates.java @@ -0,0 +1,89 @@ +package dev.hytalemodding.impulse.builtin.control.internal; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.WeakHashMap; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * PhysicsStore-keyed runtime-only state for bodies currently driven by control sessions. + */ +public final class PhysicsControlRuntimeStates { + + @Nonnull + private static final Map, PhysicsControlRuntimeState> STATES_BY_STORE = + Collections.synchronizedMap(new WeakHashMap<>()); + + private PhysicsControlRuntimeStates() { + } + + public static void markControlled(@Nonnull Ref bodyRef) { + Store store = requireStore(bodyRef, "mark a PhysicsStore body controlled"); + stateFor(store).markBodyControlled(bodyRef); + } + + public static void clearControlled(@Nullable Ref bodyRef) { + Store store = storeOrNull(bodyRef); + if (store == null || bodyRef == null) { + return; + } + PhysicsThreading.requireWorldThread(store, "clear a controlled PhysicsStore body"); + stateFor(store).clearControlledBody(bodyRef); + } + + public static boolean isControlled(@Nullable Ref bodyRef) { + Store store = storeOrNull(bodyRef); + if (store == null || bodyRef == null) { + return false; + } + PhysicsThreading.requireWorldThread(store, "check a controlled PhysicsStore body"); + return stateFor(store).isBodyControlled(bodyRef); + } + + public static void clear(@Nonnull Store store) { + Store checkedStore = Objects.requireNonNull(store, "store"); + synchronized (STATES_BY_STORE) { + PhysicsControlRuntimeState state = STATES_BY_STORE.get(checkedStore); + if (state != null) { + state.clear(); + } + } + } + + public static void clearAll() { + synchronized (STATES_BY_STORE) { + for (PhysicsControlRuntimeState state : STATES_BY_STORE.values()) { + state.clear(); + } + } + } + + @Nonnull + private static PhysicsControlRuntimeState stateFor(@Nonnull Store store) { + synchronized (STATES_BY_STORE) { + return STATES_BY_STORE.computeIfAbsent(store, _ -> new PhysicsControlRuntimeState()); + } + } + + @Nonnull + private static Store requireStore(@Nonnull Ref ref, + @Nonnull String operation) { + Store store = Objects.requireNonNull(ref, "ref").getStore(); + if (store == null) { + throw new IllegalArgumentException("PhysicsStore ref has no owning store"); + } + PhysicsThreading.requireWorldThread(store, operation); + return store; + } + + @Nullable + private static Store storeOrNull(@Nullable Ref ref) { + return ref != null ? ref.getStore() : null; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/components/PhysicsControlSessionComponent.java b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/components/PhysicsControlSessionComponent.java similarity index 55% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/components/PhysicsControlSessionComponent.java rename to impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/components/PhysicsControlSessionComponent.java index 95e5d8c9..7bd912bf 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/components/PhysicsControlSessionComponent.java +++ b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/components/PhysicsControlSessionComponent.java @@ -1,13 +1,12 @@ -package dev.hytalemodding.impulse.core.internal.modules.control.components; +package dev.hytalemodding.impulse.builtin.control.internal.components; import com.hypixel.hytale.component.Component; import com.hypixel.hytale.component.ComponentType; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; +import dev.hytalemodding.impulse.builtin.control.internal.ControlTypeRegistry; import java.util.Objects; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -18,18 +17,13 @@ public class PhysicsControlSessionComponent implements Component { @Nullable - private static ComponentType componentType; - - @Nullable - private RigidBodyKey bodyKey; + private Ref bodyRef; @Nullable - private RigidBodyKey anchorBodyKey; + private Ref anchorBodyRef; @Nullable - private JointKey controlJointKey; + private Ref controlJointRef; @Nullable private Ref targetRef; - @Nullable - private SpaceId spaceId; @Nonnull private PhysicsBodyType originalBodyType = PhysicsBodyType.DYNAMIC; @Getter @@ -46,46 +40,32 @@ public class PhysicsControlSessionComponent implements Component { public PhysicsControlSessionComponent() { } - public PhysicsControlSessionComponent(@Nonnull RigidBodyKey bodyKey, - @Nonnull RigidBodyKey anchorBodyKey, - @Nullable JointKey controlJointKey, + public PhysicsControlSessionComponent(@Nonnull Ref bodyRef, + @Nonnull Ref anchorBodyRef, + @Nullable Ref controlJointRef, @Nullable Ref targetRef, - @Nullable SpaceId spaceId, @Nonnull PhysicsBodyType originalBodyType, float grabDistance, @Nonnull Vector3f viewOffset, @Nonnull Vector3f previousTarget) { - this.bodyKey = bodyKey; - this.anchorBodyKey = anchorBodyKey; - this.controlJointKey = controlJointKey; + this.bodyRef = Objects.requireNonNull(bodyRef, "bodyRef"); + this.anchorBodyRef = Objects.requireNonNull(anchorBodyRef, "anchorBodyRef"); + this.controlJointRef = controlJointRef; this.targetRef = targetRef; - this.spaceId = spaceId; - this.originalBodyType = originalBodyType; + this.originalBodyType = Objects.requireNonNull(originalBodyType, "originalBodyType"); this.grabDistance = grabDistance; - this.viewOffset.set(viewOffset); - this.previousTarget.set(previousTarget); + this.viewOffset.set(Objects.requireNonNull(viewOffset, "viewOffset")); + this.previousTarget.set(Objects.requireNonNull(previousTarget, "previousTarget")); this.active = true; } - public static void setComponentType( - @Nonnull ComponentType type) { - componentType = Objects.requireNonNull(type, "type"); - } - - public static void clearComponentType() { - componentType = null; - } - public static boolean isComponentTypeRegistered() { - return componentType != null; + return ControlTypeRegistry.isSessionComponentTypeRegistered(); } @Nonnull public static ComponentType getComponentType() { - if (componentType == null) { - throw new IllegalStateException("Physics control session component is not registered"); - } - return componentType; + return ControlTypeRegistry.sessionComponentType(); } public void deactivate() { @@ -96,11 +76,10 @@ public void deactivate() { @Override public PhysicsControlSessionComponent clone() { PhysicsControlSessionComponent copy = new PhysicsControlSessionComponent(); - copy.bodyKey = bodyKey; - copy.anchorBodyKey = anchorBodyKey; - copy.controlJointKey = controlJointKey; + copy.bodyRef = bodyRef; + copy.anchorBodyRef = anchorBodyRef; + copy.controlJointRef = controlJointRef; copy.targetRef = targetRef; - copy.spaceId = spaceId; copy.originalBodyType = originalBodyType; copy.grabDistance = grabDistance; copy.viewOffset.set(viewOffset); diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsControlRuntimeHolderSystem.java b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsControlRuntimeHolderSystem.java similarity index 87% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsControlRuntimeHolderSystem.java rename to impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsControlRuntimeHolderSystem.java index 1a35facb..a76c1885 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsControlRuntimeHolderSystem.java +++ b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsControlRuntimeHolderSystem.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.modules.control.systems; +package dev.hytalemodding.impulse.builtin.control.internal.systems; import com.hypixel.hytale.component.AddReason; import com.hypixel.hytale.component.ComponentType; @@ -8,9 +8,9 @@ import com.hypixel.hytale.component.query.Query; import com.hypixel.hytale.component.system.HolderSystem; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.modules.control.ControlLifecycle; -import dev.hytalemodding.impulse.core.internal.modules.control.components.PhysicsControlSessionComponent; -import dev.hytalemodding.impulse.core.plugin.modules.control.ImpulseControllableComponent; +import dev.hytalemodding.impulse.builtin.control.internal.ControlLifecycle; +import dev.hytalemodding.impulse.builtin.control.internal.components.PhysicsControlSessionComponent; +import dev.hytalemodding.impulse.builtin.control.ImpulseControllableComponent; import java.util.Objects; import javax.annotation.Nonnull; diff --git a/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsControlSessionCleanup.java b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsControlSessionCleanup.java new file mode 100644 index 00000000..ba08e9a9 --- /dev/null +++ b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsControlSessionCleanup.java @@ -0,0 +1,36 @@ +package dev.hytalemodding.impulse.builtin.control.internal.systems; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.builtin.control.internal.PhysicsControlRuntimeStates; +import dev.hytalemodding.impulse.builtin.control.internal.components.PhysicsControlSessionComponent; +import javax.annotation.Nonnull; + +public final class PhysicsControlSessionCleanup { + + private PhysicsControlSessionCleanup() { + } + + public static void cleanup(@Nonnull Store store, + @Nonnull PhysicsControlSessionComponent session) { + cleanupInternal(store, session); + } + + private static void cleanupInternal(@Nonnull Store store, + @Nonnull PhysicsControlSessionComponent session) { + PhysicsKinematicControlSystem.clearMutationState(store, session.getAnchorBodyRef()); + if (!session.isActive()) { + return; + } + + Ref bodyRef = session.getBodyRef(); + if (bodyRef != null) { + PhysicsControlRuntimeStates.clearControlled(bodyRef); + } + + PhysicsStoreControlSessionMutations.applyRelease(store, session); + session.deactivate(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsControlSessionCleanupSystem.java b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsControlSessionCleanupSystem.java similarity index 78% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsControlSessionCleanupSystem.java rename to impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsControlSessionCleanupSystem.java index aaca490a..f18dff69 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsControlSessionCleanupSystem.java +++ b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsControlSessionCleanupSystem.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.modules.control.systems; +package dev.hytalemodding.impulse.builtin.control.internal.systems; import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.ComponentType; @@ -7,8 +7,9 @@ import com.hypixel.hytale.component.query.Query; import com.hypixel.hytale.component.system.RefChangeSystem; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.modules.control.ControlLifecycle; -import dev.hytalemodding.impulse.core.internal.modules.control.components.PhysicsControlSessionComponent; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.builtin.control.internal.ControlLifecycle; +import dev.hytalemodding.impulse.builtin.control.internal.components.PhysicsControlSessionComponent; import java.util.Objects; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -86,9 +87,19 @@ public Query getQuery() { private static boolean sameSessionOwner(@Nonnull PhysicsControlSessionComponent first, @Nonnull PhysicsControlSessionComponent second) { - return Objects.equals(first.getBodyKey(), second.getBodyKey()) - && Objects.equals(first.getAnchorBodyKey(), second.getAnchorBodyKey()) - && Objects.equals(first.getControlJointKey(), second.getControlJointKey()) - && Objects.equals(first.getSpaceId(), second.getSpaceId()); + return sameRef(first.getBodyRef(), second.getBodyRef()) + && sameRef(first.getAnchorBodyRef(), second.getAnchorBodyRef()) + && sameRef(first.getControlJointRef(), second.getControlJointRef()); + } + + private static boolean sameRef(@Nullable Ref first, + @Nullable Ref second) { + if (first == second) { + return true; + } + return first != null + && second != null + && first.getStore() == second.getStore() + && first.getIndex() == second.getIndex(); } } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsControllableLifecycleSystem.java b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsControllableLifecycleSystem.java similarity index 91% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsControllableLifecycleSystem.java rename to impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsControllableLifecycleSystem.java index 77879cc9..3483d5ab 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsControllableLifecycleSystem.java +++ b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsControllableLifecycleSystem.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.modules.control.systems; +package dev.hytalemodding.impulse.builtin.control.internal.systems; import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.ComponentType; @@ -7,8 +7,8 @@ import com.hypixel.hytale.component.query.Query; import com.hypixel.hytale.component.system.RefChangeSystem; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.modules.control.ControlLifecycle; -import dev.hytalemodding.impulse.core.plugin.modules.control.ImpulseControllableComponent; +import dev.hytalemodding.impulse.builtin.control.internal.ControlLifecycle; +import dev.hytalemodding.impulse.builtin.control.ImpulseControllableComponent; import java.util.Objects; import javax.annotation.Nonnull; import javax.annotation.Nullable; diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsKinematicControlSystem.java b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsKinematicControlSystem.java similarity index 52% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsKinematicControlSystem.java rename to impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsKinematicControlSystem.java index b6a98d9e..4e85543e 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsKinematicControlSystem.java +++ b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsKinematicControlSystem.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.modules.control.systems; +package dev.hytalemodding.impulse.builtin.control.internal.systems; import com.hypixel.hytale.component.ArchetypeChunk; import com.hypixel.hytale.component.CommandBuffer; @@ -10,7 +10,6 @@ import com.hypixel.hytale.component.dependency.SystemDependency; import com.hypixel.hytale.component.query.Query; import com.hypixel.hytale.component.system.tick.EntityTickingSystem; -import com.hypixel.hytale.logger.HytaleLogger; import com.hypixel.hytale.math.vector.Rotation3f; import com.hypixel.hytale.math.vector.Vector3dUtil; import com.hypixel.hytale.server.core.asset.type.model.config.Model; @@ -18,31 +17,28 @@ import com.hypixel.hytale.server.core.modules.entity.component.ModelComponent; import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.core.internal.modules.control.ControlLifecycle; -import dev.hytalemodding.impulse.core.internal.modules.control.components.PhysicsControlSessionComponent; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.systems.sync.PhysicsSyncSystem; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import it.unimi.dsi.fastutil.objects.Object2ObjectMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.builtin.control.internal.ControlLifecycle; +import dev.hytalemodding.impulse.builtin.control.internal.components.PhysicsControlSessionComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.sync.PhysicsSyncSystem; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.WeakHashMap; -import java.util.logging.Level; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import org.joml.Quaterniond; +import org.joml.Quaternionf; import org.joml.Vector3d; import org.joml.Vector3f; public class PhysicsKinematicControlSystem extends EntityTickingSystem { - private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); - private static final Set> DEPENDENCIES = Set.of( new SystemDependency<>(Order.BEFORE, PhysicsSyncSystem.class) ); @@ -52,8 +48,9 @@ public class PhysicsKinematicControlSystem extends EntityTickingSystem query; private final ThreadLocal scratch = ThreadLocal.withInitial(Scratch::new); - // Anchor updates are copied commands; keep one owner command in flight and at most one latest - // queued target per session anchor. + private static final Vector3f ZERO_VELOCITY = new Vector3f(); + private static final Quaternionf IDENTITY_ROTATION = new Quaternionf(); + // Anchor updates are copied into PhysicsStore entities; avoid rewriting unchanged targets. @Nonnull private static final Map, ControlMutationState> STATES_BY_STORE = Collections.synchronizedMap(new WeakHashMap<>()); @@ -90,24 +87,16 @@ public void tick(float dt, return; } - PhysicsWorldRuntimeResource resource = PhysicsWorldRuntimeResource.require(store); - RigidBodyKey bodyKey = session.getBodyKey(); - RigidBodyKey anchorBodyKey = session.getAnchorBodyKey(); + Ref bodyRef = session.getBodyRef(); + Ref anchorBodyRef = session.getAnchorBodyRef(); Ref targetRef = session.getTargetRef(); - if (bodyKey == null - || anchorBodyKey == null - || !resource.hasPublishedOrPendingBodyRegistration(bodyKey) - || !resource.hasPublishedOrPendingBodyRegistration(anchorBodyKey) + if (bodyRef == null + || anchorBodyRef == null + || !bodyRef.isValid() + || !anchorBodyRef.isValid() || (targetRef != null && !targetRef.isValid())) { - stateFor(store).clear(anchorBodyKey); - PhysicsControlSessionCleanup.cleanup(resource, session); - commandBuffer.removeComponent(chunk.getReferenceTo(index), sessionType); - return; - } - - if (session.getSpaceId() != null && !resource.hasSpace(session.getSpaceId())) { - stateFor(store).clear(anchorBodyKey); - PhysicsControlSessionCleanup.cleanup(resource, session); + stateFor(store).clear(anchorBodyRef); + PhysicsControlSessionCleanup.cleanup(store, session); commandBuffer.removeComponent(chunk.getReferenceTo(index), sessionType); return; } @@ -117,14 +106,11 @@ public void tick(float dt, eye.set(transform.getPosition()); eye.y += eyeHeight(chunk, index, chunk.getReferenceTo(index), store); - Vector3d direction = lookDirection(chunk, index, transform, local.direction); + Rotation3f viewRotation = rotation(chunk, index, transform); + Vector3d direction = lookDirection(viewRotation, local.direction); Vector3f viewOffset = session.getViewOffset(); - local.right.set(Vector3dUtil.RIGHT); - local.up.set(Vector3dUtil.UP); - local.rotation.identity(); - rotation(chunk, index, transform).getQuaternion(local.rotation); - local.rotation.transform(local.right); - local.rotation.transform(local.up); + viewRotation.transform(Vector3dUtil.RIGHT, local.right); + viewRotation.transform(Vector3dUtil.UP, local.up); local.target.set( (float) (eye.x + direction.x * session.getGrabDistance() @@ -150,36 +136,51 @@ public void tick(float dt, previousTarget.set(local.target); ControlMutationState state = stateFor(store); - ControlAnchorUpdate update = new ControlAnchorUpdate(bodyKey, - anchorBodyKey, + ControlAnchorUpdate update = new ControlAnchorUpdate(bodyRef, + anchorBodyRef, local.target, releaseVelocity); - ControlAnchorUpdate readyUpdate = state.selectReadyUpdate(anchorBodyKey, update); + ControlAnchorUpdate readyUpdate = state.selectReadyUpdate(anchorBodyRef, update); if (readyUpdate == null) { return; } - PhysicsMutationHandle handle = PhysicsMutationHandle.fromCompletion( - "update kinematic control anchor", - null, - resource.submitCommands(0L, 4, commands -> commands - .setBodyType(readyUpdate.anchorBodyKey(), PhysicsBodyType.KINEMATIC) - .setBodyPosition(readyUpdate.anchorBodyKey(), - readyUpdate.target().x, - readyUpdate.target().y, - readyUpdate.target().z, - false) - .setBodyVelocity(readyUpdate.anchorBodyKey(), - readyUpdate.releaseVelocity().x, - readyUpdate.releaseVelocity().y, - readyUpdate.releaseVelocity().z, - 0.0f, - 0.0f, - 0.0f, - true) - .activateBody(readyUpdate.bodyKey())) - .completionSummary()); - state.trackPendingMutation(anchorBodyKey, handle, readyUpdate); + PhysicsStoreControlTargets physicsStoreTargets = + resolvePhysicsStoreTargets(store, bodyRef, anchorBodyRef); + if (physicsStoreTargets != null) { + physicsStoreTargets.apply(readyUpdate); + state.trackSubmittedMutation(anchorBodyRef, readyUpdate); + return; + } + + stateFor(store).clear(anchorBodyRef); + PhysicsControlSessionCleanup.cleanup(store, session); + commandBuffer.removeComponent(chunk.getReferenceTo(index), sessionType); + } + + @Nullable + private static PhysicsStoreControlTargets resolvePhysicsStoreTargets( + @Nonnull Store store, + @Nonnull Ref bodyRef, + @Nonnull Ref anchorBodyRef) { + Store physics = PhysicsThreading.store( + store.getExternalData().getWorld()); + PhysicsThreading.requireWorldThread(physics, + "resolve PhysicsStore kinematic control targets"); + if (!validBodyRef(physics, bodyRef) || !validBodyRef(physics, anchorBodyRef)) { + return null; + } + return new PhysicsStoreControlTargets( + physics, + bodyRef, + anchorBodyRef); + } + + private static boolean validBodyRef(@Nonnull Store physics, + @Nonnull Ref ref) { + return ref.getStore() == physics + && ref.isValid() + && physics.getComponent(ref, BodyComponent.getComponentType()) != null; } private float eyeHeight(@Nonnull ArchetypeChunk chunk, @@ -200,14 +201,9 @@ private float eyeHeight(@Nonnull ArchetypeChunk chunk, } @Nonnull - private Vector3d lookDirection(@Nonnull ArchetypeChunk chunk, - int index, - @Nonnull TransformComponent transform, + private Vector3d lookDirection(@Nonnull Rotation3f rotation, @Nonnull Vector3d out) { - Rotation3f rotation = rotation(chunk, index, transform); - Quaterniond quaternion = rotation.getQuaternion(new Quaterniond()); - out.set(Vector3dUtil.FORWARD); - quaternion.transform(out); + rotation.transform(Vector3dUtil.FORWARD, out); if (out.lengthSquared() == 0.0) { out.set(Vector3dUtil.FORWARD); } else { @@ -232,14 +228,14 @@ static ControlMutationState stateFor(@Nonnull Store store) { } public static void clearMutationState(@Nonnull Store store, - @Nullable RigidBodyKey anchorBodyKey) { - if (anchorBodyKey != null) { - stateFor(store).clear(anchorBodyKey); + @Nullable Ref anchorBodyRef) { + if (anchorBodyRef != null) { + stateFor(store).clear(anchorBodyRef); } } - record ControlAnchorUpdate(@Nonnull RigidBodyKey bodyKey, - @Nonnull RigidBodyKey anchorBodyKey, + record ControlAnchorUpdate(@Nonnull Ref bodyRef, + @Nonnull Ref anchorBodyRef, @Nonnull Vector3f target, @Nonnull Vector3f releaseVelocity) { @@ -249,87 +245,67 @@ record ControlAnchorUpdate(@Nonnull RigidBodyKey bodyKey, } } - static final class ControlMutationState { + private record PhysicsStoreControlTargets(@Nonnull Store store, + @Nonnull Ref bodyRef, + @Nonnull Ref anchorBodyRef) { + + private void apply(@Nonnull ControlAnchorUpdate update) { + store.putComponent(anchorBodyRef, + TargetComponent.getComponentType(), + target(update.target(), + update.releaseVelocity(), + true, + true)); + store.putComponent(bodyRef, + TargetComponent.getComponentType(), + target(update.target(), + ZERO_VELOCITY, + false, + false)); + } - /* - * Coalescing happens on the tick thread: completion callbacks only clear the in-flight - * marker. The next tick decides whether the latest queued target still belongs to an active - * session and is different enough to submit. - * - * FIXME: Replace this coalescing workaround when strict control scheduling lands. - */ - @Nonnull - private final Object2ObjectMap> pendingMutations = - new Object2ObjectOpenHashMap<>(); - @Nonnull - private final Object2ObjectMap queuedUpdates = - new Object2ObjectOpenHashMap<>(); @Nonnull - private final Object2ObjectMap submittedUpdates = - new Object2ObjectOpenHashMap<>(); - - synchronized boolean hasPendingMutation(@Nonnull RigidBodyKey bodyKey) { - PhysicsMutationHandle handle = pendingMutations.get(bodyKey); - if (handle == null) { - return false; - } - if (!handle.isDone()) { - return true; - } - pendingMutations.remove(bodyKey); - return false; + private static TargetComponent target(@Nonnull Vector3f position, + @Nonnull Vector3f linearVelocity, + boolean transformEnabled, + boolean velocityEnabled) { + TargetComponent target = new TargetComponent(); + target.setActive(true); + target.setPosition(position); + target.setRotation(IDENTITY_ROTATION); + target.setLinearVelocity(linearVelocity); + target.setAngularVelocity(ZERO_VELOCITY); + target.setTransformEnabled(transformEnabled); + target.setVelocityEnabled(velocityEnabled); + target.setActivate(true); + return target; } + } + + static final class ControlMutationState { + + @Nonnull + private final Int2ObjectMap submittedUpdates = + new Int2ObjectOpenHashMap<>(); @Nullable - synchronized ControlAnchorUpdate selectReadyUpdate(@Nonnull RigidBodyKey bodyKey, + synchronized ControlAnchorUpdate selectReadyUpdate(@Nonnull Ref anchorBodyRef, @Nonnull ControlAnchorUpdate currentUpdate) { - PhysicsMutationHandle handle = pendingMutations.get(bodyKey); - if (handle != null && !handle.isDone()) { - queuedUpdates.put(bodyKey, currentUpdate); + ControlAnchorUpdate submittedUpdate = submittedUpdates.get(anchorBodyRef.getIndex()); + if (sameTarget(currentUpdate, submittedUpdate)) { return null; } - if (handle != null) { - pendingMutations.remove(bodyKey); - } - - ControlAnchorUpdate queuedUpdate = queuedUpdates.remove(bodyKey); - ControlAnchorUpdate submittedUpdate = submittedUpdates.get(bodyKey); - if (queuedUpdate != null) { - if (!sameTarget(queuedUpdate, submittedUpdate)) { - return queuedUpdate; - } - if (sameTarget(currentUpdate, submittedUpdate)) { - return null; - } - } return currentUpdate; } - synchronized void trackPendingMutation(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsMutationHandle handle, + synchronized void trackSubmittedMutation(@Nonnull Ref anchorBodyRef, @Nonnull ControlAnchorUpdate submittedUpdate) { - submittedUpdates.put(bodyKey, submittedUpdate); - if (handle.isDone()) { - logImmediateFailure(handle); - return; - } - pendingMutations.put(bodyKey, handle); - handle.completion().whenComplete((ignored, _) -> clear(bodyKey, handle)); + submittedUpdates.put(anchorBodyRef.getIndex(), submittedUpdate); } - synchronized void clear(@Nullable RigidBodyKey bodyKey) { - if (bodyKey != null) { - pendingMutations.remove(bodyKey); - queuedUpdates.remove(bodyKey); - submittedUpdates.remove(bodyKey); - } - } - - private synchronized void clear(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsMutationHandle expectedHandle) { - PhysicsMutationHandle current = pendingMutations.get(bodyKey); - if (current == expectedHandle) { - pendingMutations.remove(bodyKey); + synchronized void clear(@Nullable Ref anchorBodyRef) { + if (anchorBodyRef != null) { + submittedUpdates.remove(anchorBodyRef.getIndex()); } } @@ -340,15 +316,6 @@ private static boolean sameTarget(@Nonnull ControlAnchorUpdate first, && Float.compare(first.target().y, second.target().y) == 0 && Float.compare(first.target().z, second.target().z) == 0; } - - private static void logImmediateFailure(@Nonnull PhysicsMutationHandle handle) { - Throwable failure = handle.failure(); - if (failure != null) { - LOGGER.at(Level.WARNING).log( - "Kinematic control anchor update could not be queued: %s", - failure.getMessage()); - } - } } private static final class Scratch { @@ -357,7 +324,6 @@ private static final class Scratch { private final Vector3d direction = new Vector3d(); private final Vector3d right = new Vector3d(); private final Vector3d up = new Vector3d(); - private final Quaterniond rotation = new Quaterniond(); private final Vector3f target = new Vector3f(); } diff --git a/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsStoreControlSessionMutations.java b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsStoreControlSessionMutations.java new file mode 100644 index 00000000..e2c01984 --- /dev/null +++ b/impulse-builtins/control/src/main/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsStoreControlSessionMutations.java @@ -0,0 +1,127 @@ +package dev.hytalemodding.impulse.builtin.control.internal.systems; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.RemoveReason; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.builtin.control.internal.components.PhysicsControlSessionComponent; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsStoreRowCleanup; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.plugin.components.BodyCommandComponent; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3f; + +/** + * Direct PhysicsStore entity mutations for kinematic control lifecycle cleanup. + */ +public final class PhysicsStoreControlSessionMutations { + + private static final Vector3f ZERO = new Vector3f(); + + private PhysicsStoreControlSessionMutations() { + } + + public static void applyRelease(@Nonnull Store store, + @Nonnull PhysicsControlSessionComponent session) { + Store physicsStore = PhysicsThreading.store( + store.getExternalData().getWorld()); + PhysicsThreading.requireWorldThread(physicsStore, + "apply PhysicsStore control-session release mutations"); + + Ref controlJointRef = session.getControlJointRef(); + if (controlJointRef != null) { + removeJointRow(physicsStore, controlJointRef); + } + + Ref bodyRef = session.getBodyRef(); + if (bodyRef != null) { + restoreControlledBody(physicsStore, + bodyRef, + session.getOriginalBodyType(), + releaseVelocity(session)); + } + + Ref anchorBodyRef = session.getAnchorBodyRef(); + if (anchorBodyRef != null) { + removeBodyRow(physicsStore, anchorBodyRef); + } + } + + private static void restoreControlledBody(@Nonnull Store store, + @Nonnull Ref bodyRef, + @Nonnull PhysicsBodyType originalBodyType, + @Nonnull Vector3f releaseVelocity) { + if (!isValidStoreRef(store, bodyRef) + || store.getComponent(bodyRef, BodyComponent.getComponentType()) == null) { + return; + } + PhysicsBodies.appendCommand(store, + bodyRef, + BodyCommandComponent.setType(originalBodyType, true)); + PhysicsBodies.appendCommand(store, + bodyRef, + BodyCommandComponent.setVelocity(releaseVelocity, ZERO, true)); + } + + private static void removeJointRow(@Nonnull Store store, + @Nonnull Ref ref) { + if (!isValidStoreRef(store, ref)) { + return; + } + UuidComponent uuid = store.getComponent(ref, UuidComponent.getComponentType()); + if (uuid == null) { + removeRow(store, ref); + return; + } + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + PhysicsStoreRowCleanup.removeRuntimeJoint(store, runtime, uuid.getUuid(), ref); + PhysicsStoreRowCleanup.removeJointEntity(store, uuid.getUuid(), ref); + } + + private static void removeBodyRow(@Nonnull Store store, + @Nonnull Ref ref) { + if (!isValidStoreRef(store, ref)) { + return; + } + UuidComponent uuid = store.getComponent(ref, UuidComponent.getComponentType()); + if (uuid == null) { + removeRow(store, ref); + return; + } + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + PhysicsStoreRowCleanup.removeRuntimeBody(store, runtime, uuid.getUuid(), ref, null); + PhysicsStoreRowCleanup.removeBodyEntity(store, uuid.getUuid(), ref); + } + + private static void removeRow(@Nonnull Store store, + @Nonnull Ref ref) { + if (!isValidStoreRef(store, ref)) { + return; + } + UuidComponent uuid = store.getComponent(ref, UuidComponent.getComponentType()); + if (uuid != null) { + store.getExternalData().removeRefForUUID(uuid.getUuid(), ref); + } + store.removeEntity(ref, store.getRegistry().newHolder(), RemoveReason.REMOVE); + } + + private static boolean isValidStoreRef(@Nonnull Store store, + @Nullable Ref ref) { + return ref != null && ref.getStore() == store && ref.isValid(); + } + + @Nonnull + private static Vector3f releaseVelocity(@Nonnull PhysicsControlSessionComponent session) { + if (session.getOriginalBodyType() == PhysicsBodyType.DYNAMIC) { + return session.getReleaseVelocity(); + } + return new Vector3f(); + } +} diff --git a/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/ControlLifecycleTest.java b/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/ControlLifecycleTest.java new file mode 100644 index 00000000..42c91890 --- /dev/null +++ b/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/ControlLifecycleTest.java @@ -0,0 +1,258 @@ +package dev.hytalemodding.impulse.builtin.control.internal; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.thread.TickingThread; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.builtin.control.internal.components.PhysicsControlSessionComponent; +import dev.hytalemodding.impulse.builtin.control.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.builtin.control.ImpulseControllableComponent; +import dev.hytalemodding.impulse.builtin.control.PhysicsControlSessions; +import java.lang.reflect.Method; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3f; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ControlLifecycleTest { + + @BeforeEach + @AfterEach + void disableLifecycle() { + ControlLifecycle.disable(); + ControlTypeRegistry.clearComponentTypes(); + } + + @Test + void lifecycleStartsDisabled() { + assertFalse(ControlLifecycle.isEnabled()); + } + + @Test + void disablingLifecycleWithoutRegisteredSessionComponentDoesNotThrow() { + ControlTypeRegistry.clearComponentTypes(); + ControlLifecycle.enable(); + + assertDoesNotThrow(ControlLifecycle::disable); + + assertFalse(ControlLifecycle.isEnabled()); + } + + @Test + void disablingLifecycleClearsRegisteredControlledBodies() { + ControlLifecycle.enable(); + World world = TestInstanceFactory.world("control-physics-world"); + ComponentRegistry registry = new ComponentRegistry<>(); + Store store = registry.addStore( + new PhysicsStore(world), + EmptyResourceStorage.get()); + Ref bodyRef = new TestPhysicsRef(store, 7); + + runOnWorldThread(world, () -> { + PhysicsControlRuntimeStates.markControlled(bodyRef); + assertTrue(PhysicsControlRuntimeStates.isControlled(bodyRef)); + }); + + ControlLifecycle.disable(); + + runOnWorldThread(world, () -> assertFalse(PhysicsControlRuntimeStates.isControlled(bodyRef))); + registry.shutdown(); + } + + @Test + void publicFacadeReadsPhysicsStoreBodyControlState() { + ControlLifecycle.enable(); + World world = TestInstanceFactory.world("control-body-state-world"); + ComponentRegistry registry = new ComponentRegistry<>(); + Store store = registry.addStore( + new PhysicsStore(world), + EmptyResourceStorage.get()); + Ref bodyRef = new TestPhysicsRef(store, 7); + try { + runOnWorldThread(world, () -> { + assertFalse(PhysicsControlSessions.isBodyControlled(bodyRef)); + + PhysicsControlRuntimeStates.markControlled(bodyRef); + + assertTrue(PhysicsControlSessions.isBodyControlled(bodyRef)); + }); + } finally { + registry.shutdown(); + } + } + + @Test + void publicFacadeMatchesControllerSessionBody() { + ControlLifecycle.enable(); + ComponentRegistry entityRegistry = new ComponentRegistry<>(); + ControlTypeRegistry.registerComponentTypes(entityRegistry); + World world = TestInstanceFactory.world("control-session-body-world"); + ComponentRegistry physicsRegistry = new ComponentRegistry<>(); + Store physicsStore = physicsRegistry.addStore( + new PhysicsStore(world), + EmptyResourceStorage.get()); + Store entityStore = entityRegistry.addStore(new EntityStore(world), + EmptyResourceStorage.get()); + Ref controllerRef = addEmptyEntity(entityStore); + Ref otherControllerRef = addEmptyEntity(entityStore); + Ref bodyRef = new TestPhysicsRef(physicsStore, 7); + Ref otherBodyRef = new TestPhysicsRef(physicsStore, 8); + Ref anchorRef = new TestPhysicsRef(physicsStore, 9); + try { + entityStore.putComponent(controllerRef, + PhysicsControlSessionComponent.getComponentType(), + session(bodyRef, anchorRef)); + + assertTrue(PhysicsControlSessions.hasSessionForBody(entityStore, + controllerRef, + bodyRef)); + assertFalse(PhysicsControlSessions.hasSessionForBody(entityStore, + controllerRef, + otherBodyRef)); + assertFalse(PhysicsControlSessions.hasSessionForBody(entityStore, + otherControllerRef, + bodyRef)); + } finally { + entityRegistry.shutdown(); + physicsRegistry.shutdown(); + } + } + + @Test + void startSessionRejectsBodyControlledByAnotherController() { + ControlLifecycle.enable(); + ComponentRegistry entityRegistry = new ComponentRegistry<>(); + ControlTypeRegistry.registerComponentTypes(entityRegistry); + World world = TestInstanceFactory.world("control-session-reject-world"); + ComponentRegistry physicsRegistry = new ComponentRegistry<>(); + Store physicsStore = physicsRegistry.addStore( + new PhysicsStore(world), + EmptyResourceStorage.get()); + Store entityStore = entityRegistry.addStore(new EntityStore(world), + EmptyResourceStorage.get()); + Ref controllerRef = addEmptyEntity(entityStore); + Ref bodyRef = new TestPhysicsRef(physicsStore, 7); + Ref anchorRef = new TestPhysicsRef(physicsStore, 8); + try { + runOnWorldThread(world, () -> { + PhysicsControlRuntimeStates.markControlled(bodyRef); + + assertThrows(IllegalStateException.class, + () -> PhysicsControlSessions.startSession(entityStore, + controllerRef, + bodyRef, + anchorRef, + null, + null, + PhysicsBodyType.DYNAMIC, + 4.0f, + new Vector3f(), + new Vector3f())); + }); + } finally { + entityRegistry.shutdown(); + physicsRegistry.shutdown(); + } + } + + @Test + void controlSessionsAreAvailableOnlyWhenLifecycleAndComponentTypesAreRegistered() { + assertFalse(PhysicsControlSessions.isAvailable()); + + ControlLifecycle.enable(); + assertFalse(PhysicsControlSessions.isAvailable()); + + ComponentRegistry registry = new ComponentRegistry<>(); + ControlTypeRegistry.registerComponentTypes(registry); + + assertTrue(PhysicsControlSessions.isAvailable()); + + ControlLifecycle.disable(); + + assertFalse(PhysicsControlSessions.isAvailable()); + } + + @Test + void disablingLifecycleSkipsStoresWhoseWorldThreadHasStopped() { + ControlLifecycle.enable(); + ComponentRegistry registry = new ComponentRegistry<>(); + ControlTypeRegistry.registerComponentTypes(registry); + Store store = registry.addStore( + new EntityStore(TestInstanceFactory.world("stopped-control-world")), + EmptyResourceStorage.get()); + ControlLifecycle.registerStore(store); + + assertDoesNotThrow(ControlLifecycle::disable); + + assertFalse(ControlLifecycle.isEnabled()); + registry.shutdown(); + } + + @Nonnull + private static Ref addEmptyEntity(@Nonnull Store store) { + Holder holder = store.getRegistry().newHolder(); + Ref ref = store.addEntity(holder, AddReason.SPAWN); + if (ref == null) { + throw new AssertionError("Failed to add test entity"); + } + return ref; + } + + @Nonnull + private static PhysicsControlSessionComponent session(@Nonnull Ref bodyRef, + @Nonnull Ref anchorRef) { + return new PhysicsControlSessionComponent(bodyRef, + anchorRef, + null, + null, + PhysicsBodyType.DYNAMIC, + 4.0f, + new Vector3f(), + new Vector3f()); + } + + private static final class TestPhysicsRef extends Ref { + + private TestPhysicsRef(Store store, int index) { + super(store, index); + } + + @Override + public boolean isValid() { + return true; + } + } + + private static void runOnWorldThread(@Nonnull World world, @Nonnull Runnable task) { + setThread(world, Thread.currentThread()); + try { + task.run(); + } finally { + setThread(world, null); + } + } + + private static void setThread(@Nonnull World world, @Nullable Thread thread) { + try { + Method setThread = TickingThread.class.getDeclaredMethod("setThread", Thread.class); + setThread.setAccessible(true); + setThread.invoke(world, thread); + } catch (ReflectiveOperationException exception) { + throw new AssertionError("Failed to bind test world thread", exception); + } + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/modules/control/ImpulseControllableComponentTest.java b/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/ImpulseBackendRegistryControllableComponentRegistrationTest.java similarity index 68% rename from impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/modules/control/ImpulseControllableComponentTest.java rename to impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/ImpulseBackendRegistryControllableComponentRegistrationTest.java index 6c0a3623..5aed4381 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/modules/control/ImpulseControllableComponentTest.java +++ b/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/ImpulseBackendRegistryControllableComponentRegistrationTest.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.plugin.modules.control; +package dev.hytalemodding.impulse.builtin.control.internal; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; @@ -8,33 +8,31 @@ import com.hypixel.hytale.component.ComponentRegistry; import com.hypixel.hytale.component.ComponentType; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.builtin.control.ImpulseControllableComponent; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -class ImpulseControllableComponentTest { +class ImpulseBackendRegistryControllableComponentRegistrationTest { @AfterEach void clearRegistration() { - ImpulseControllableComponent.clearComponentType(); + ControlTypeRegistry.clearComponentTypes(); } @Test - void componentTypeIsOwnedByControlModuleRegistration() { + void componentTypeIsOwnedByControlSubPluginRegistration() { assertFalse(ImpulseControllableComponent.isComponentTypeRegistered()); assertThrows(IllegalStateException.class, ImpulseControllableComponent::getComponentType); ComponentRegistry registry = new ComponentRegistry<>(); + ControlTypeRegistry.registerComponentTypes(registry); ComponentType type = - registry.registerComponent(ImpulseControllableComponent.class, - "ImpulseControllable", - ImpulseControllableComponent.CODEC); - - ImpulseControllableComponent.setComponentType(type); + ImpulseControllableComponent.getComponentType(); assertTrue(ImpulseControllableComponent.isComponentTypeRegistered()); assertSame(type, ImpulseControllableComponent.getComponentType()); - ImpulseControllableComponent.clearComponentType(); + ControlTypeRegistry.clearComponentTypes(); assertFalse(ImpulseControllableComponent.isComponentTypeRegistered()); } diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/control/components/PhysicsControlSessionComponentTest.java b/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/components/PhysicsControlSessionComponentTest.java similarity index 70% rename from impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/control/components/PhysicsControlSessionComponentTest.java rename to impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/components/PhysicsControlSessionComponentTest.java index 2564d72a..080c112d 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/control/components/PhysicsControlSessionComponentTest.java +++ b/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/components/PhysicsControlSessionComponentTest.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.modules.control.components; +package dev.hytalemodding.impulse.builtin.control.internal.components; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertSame; @@ -8,6 +8,7 @@ import com.hypixel.hytale.component.ComponentRegistry; import com.hypixel.hytale.component.ComponentType; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.builtin.control.internal.ControlTypeRegistry; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -15,22 +16,20 @@ class PhysicsControlSessionComponentTest { @AfterEach void clearRegistration() { - PhysicsControlSessionComponent.clearComponentType(); + ControlTypeRegistry.clearComponentTypes(); } @Test - void componentTypeCanBeClearedWhenControlModuleUnloads() { + void componentTypeCanBeClearedWhenControlBuiltinUnloads() { ComponentRegistry registry = new ComponentRegistry<>(); + ControlTypeRegistry.registerComponentTypes(registry); ComponentType type = - registry.registerComponent(PhysicsControlSessionComponent.class, - PhysicsControlSessionComponent::new); - - PhysicsControlSessionComponent.setComponentType(type); + PhysicsControlSessionComponent.getComponentType(); assertTrue(PhysicsControlSessionComponent.isComponentTypeRegistered()); assertSame(type, PhysicsControlSessionComponent.getComponentType()); - PhysicsControlSessionComponent.clearComponentType(); + ControlTypeRegistry.clearComponentTypes(); assertFalse(PhysicsControlSessionComponent.isComponentTypeRegistered()); assertThrows(IllegalStateException.class, PhysicsControlSessionComponent::getComponentType); diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsControlSystemRegistrationTest.java b/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsControlSystemRegistrationTest.java similarity index 68% rename from impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsControlSystemRegistrationTest.java rename to impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsControlSystemRegistrationTest.java index ec360056..0289a9c1 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsControlSystemRegistrationTest.java +++ b/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsControlSystemRegistrationTest.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.modules.control.systems; +package dev.hytalemodding.impulse.builtin.control.internal.systems; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; @@ -10,10 +10,11 @@ import com.hypixel.hytale.component.Holder; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.modules.control.ControlLifecycle; -import dev.hytalemodding.impulse.core.internal.modules.control.components.PhysicsControlSessionComponent; -import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; -import dev.hytalemodding.impulse.core.plugin.modules.control.ImpulseControllableComponent; +import dev.hytalemodding.impulse.builtin.control.internal.ControlLifecycle; +import dev.hytalemodding.impulse.builtin.control.internal.ControlTypeRegistry; +import dev.hytalemodding.impulse.builtin.control.internal.components.PhysicsControlSessionComponent; +import dev.hytalemodding.impulse.builtin.control.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.builtin.control.ImpulseControllableComponent; import java.lang.reflect.Field; import javax.annotation.Nonnull; import org.junit.jupiter.api.AfterEach; @@ -24,18 +25,21 @@ class PhysicsControlSystemRegistrationTest { @AfterEach void clearRegistrations() { ControlLifecycle.disable(); - ImpulseControllableComponent.clearComponentType(); - PhysicsControlSessionComponent.clearComponentType(); + ControlTypeRegistry.clearComponentTypes(); } @Test void sessionCleanupSystemCapturesCurrentSessionComponentType() { - ComponentType first = registerSessionType(); - PhysicsControlSessionComponent.setComponentType(first); + ComponentRegistry firstRegistry = new ComponentRegistry<>(); + ControlTypeRegistry.registerComponentTypes(firstRegistry); + ComponentType first = + PhysicsControlSessionComponent.getComponentType(); PhysicsControlSessionCleanupSystem firstSystem = new PhysicsControlSessionCleanupSystem(); - ComponentType second = registerSessionType(); - PhysicsControlSessionComponent.setComponentType(second); + ComponentRegistry secondRegistry = new ComponentRegistry<>(); + ControlTypeRegistry.registerComponentTypes(secondRegistry); + ComponentType second = + PhysicsControlSessionComponent.getComponentType(); PhysicsControlSessionCleanupSystem secondSystem = new PhysicsControlSessionCleanupSystem(); assertSame(first, firstSystem.componentType()); @@ -44,14 +48,16 @@ void sessionCleanupSystemCapturesCurrentSessionComponentType() { @Test void controllableLifecycleSystemCapturesCurrentControllableComponentType() { + ComponentRegistry firstRegistry = new ComponentRegistry<>(); + ControlTypeRegistry.registerComponentTypes(firstRegistry); ComponentType first = - registerControllableType(); - ImpulseControllableComponent.setComponentType(first); + ImpulseControllableComponent.getComponentType(); PhysicsControllableLifecycleSystem firstSystem = new PhysicsControllableLifecycleSystem(); + ComponentRegistry secondRegistry = new ComponentRegistry<>(); + ControlTypeRegistry.registerComponentTypes(secondRegistry); ComponentType second = - registerControllableType(); - ImpulseControllableComponent.setComponentType(second); + ImpulseControllableComponent.getComponentType(); PhysicsControllableLifecycleSystem secondSystem = new PhysicsControllableLifecycleSystem(); assertSame(first, firstSystem.componentType()); @@ -60,20 +66,20 @@ void controllableLifecycleSystemCapturesCurrentControllableComponentType() { @Test void holderSystemCapturesCurrentControlComponentTypes() throws ReflectiveOperationException { + ComponentRegistry firstRegistry = new ComponentRegistry<>(); + ControlTypeRegistry.registerComponentTypes(firstRegistry); ComponentType firstControllable = - registerControllableType(); + ImpulseControllableComponent.getComponentType(); ComponentType firstSession = - registerSessionType(); - ImpulseControllableComponent.setComponentType(firstControllable); - PhysicsControlSessionComponent.setComponentType(firstSession); + PhysicsControlSessionComponent.getComponentType(); PhysicsControlRuntimeHolderSystem firstSystem = new PhysicsControlRuntimeHolderSystem(); + ComponentRegistry secondRegistry = new ComponentRegistry<>(); + ControlTypeRegistry.registerComponentTypes(secondRegistry); ComponentType secondControllable = - registerControllableType(); + ImpulseControllableComponent.getComponentType(); ComponentType secondSession = - registerSessionType(); - ImpulseControllableComponent.setComponentType(secondControllable); - PhysicsControlSessionComponent.setComponentType(secondSession); + PhysicsControlSessionComponent.getComponentType(); PhysicsControlRuntimeHolderSystem secondSystem = new PhysicsControlRuntimeHolderSystem(); assertSame(firstControllable, field(firstSystem, "controllableType")); @@ -106,19 +112,6 @@ void holderLoadKeepsControllableMarker() { registry.shutdown(); } - private static ComponentType registerControllableType() { - ComponentRegistry registry = new ComponentRegistry<>(); - return registry.registerComponent(ImpulseControllableComponent.class, - "ImpulseControllable", - ImpulseControllableComponent.CODEC); - } - - private static ComponentType registerSessionType() { - ComponentRegistry registry = new ComponentRegistry<>(); - return registry.registerComponent(PhysicsControlSessionComponent.class, - PhysicsControlSessionComponent::new); - } - private static Object field(Object target, String name) throws ReflectiveOperationException { Field field = target.getClass().getDeclaredField(name); field.setAccessible(true); diff --git a/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsKinematicControlSystemTest.java b/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsKinematicControlSystemTest.java new file mode 100644 index 00000000..5523cbb4 --- /dev/null +++ b/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsKinematicControlSystemTest.java @@ -0,0 +1,134 @@ +package dev.hytalemodding.impulse.builtin.control.internal.systems; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.builtin.control.internal.systems.PhysicsKinematicControlSystem.ControlAnchorUpdate; +import dev.hytalemodding.impulse.builtin.control.internal.systems.PhysicsKinematicControlSystem.ControlMutationState; +import dev.hytalemodding.impulse.builtin.control.internal.testsupport.TestInstanceFactory; +import javax.annotation.Nonnull; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class PhysicsKinematicControlSystemTest { + + @Test + void controlAnchorUpdateCopiesMutableVectors() { + Ref bodyRef = new TestPhysicsRef(1); + Ref anchorBodyRef = new TestPhysicsRef(2); + Vector3f target = new Vector3f(1.0f, 2.0f, 3.0f); + Vector3f releaseVelocity = new Vector3f(4.0f, 5.0f, 6.0f); + ControlAnchorUpdate update = new ControlAnchorUpdate(bodyRef, + anchorBodyRef, + target, + releaseVelocity); + target.zero(); + releaseVelocity.zero(); + + assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), update.target()); + assertEquals(new Vector3f(4.0f, 5.0f, 6.0f), update.releaseVelocity()); + } + + @Test + void submittedControlMutationSuppressesIdenticalTarget() { + ControlMutationState state = new ControlMutationState(); + Ref bodyRef = new TestPhysicsRef(1); + Ref anchorBodyRef = new TestPhysicsRef(2); + ControlAnchorUpdate first = update(bodyRef, anchorBodyRef, 1.0f); + ControlAnchorUpdate sameTarget = update(bodyRef, anchorBodyRef, 1.0f); + ControlAnchorUpdate changedTarget = update(bodyRef, anchorBodyRef, 2.0f); + + state.trackSubmittedMutation(anchorBodyRef, first); + + assertNull(state.selectReadyUpdate(anchorBodyRef, sameTarget)); + assertSame(changedTarget, state.selectReadyUpdate(anchorBodyRef, changedTarget)); + } + + @Test + void clearingControlMutationStateAllowsIdenticalTargetRetry() { + ControlMutationState state = new ControlMutationState(); + Ref bodyRef = new TestPhysicsRef(1); + Ref anchorBodyRef = new TestPhysicsRef(2); + ControlAnchorUpdate first = update(bodyRef, anchorBodyRef, 1.0f); + ControlAnchorUpdate retry = update(bodyRef, anchorBodyRef, 1.0f); + + state.trackSubmittedMutation(anchorBodyRef, first); + state.clear(anchorBodyRef); + + assertSame(retry, state.selectReadyUpdate(anchorBodyRef, retry)); + } + + @Test + void trackingSubmittedControlMutationUpdatesSuppressionTarget() { + ControlMutationState state = new ControlMutationState(); + Ref bodyRef = new TestPhysicsRef(1); + Ref anchorBodyRef = new TestPhysicsRef(2); + ControlAnchorUpdate first = update(bodyRef, anchorBodyRef, 1.0f); + ControlAnchorUpdate second = update(bodyRef, anchorBodyRef, 2.0f); + ControlAnchorUpdate sameSecondTarget = update(bodyRef, anchorBodyRef, 2.0f); + ControlAnchorUpdate third = update(bodyRef, anchorBodyRef, 3.0f); + + state.trackSubmittedMutation(anchorBodyRef, first); + assertSame(second, state.selectReadyUpdate(anchorBodyRef, second)); + + state.trackSubmittedMutation(anchorBodyRef, second); + + assertNull(state.selectReadyUpdate(anchorBodyRef, sameSecondTarget)); + assertSame(third, state.selectReadyUpdate(anchorBodyRef, third)); + } + + @Test + void clearingSystemMutationStateAfterReleaseAllowsIdenticalTargetRetry() { + ComponentRegistry registry = new ComponentRegistry<>(); + Store store = registry.addStore( + new EntityStore(TestInstanceFactory.world("control-release-mutation-state-test")), + EmptyResourceStorage.get()); + try { + ControlMutationState state = PhysicsKinematicControlSystem.stateFor(store); + Ref bodyRef = new TestPhysicsRef(1); + Ref anchorBodyRef = new TestPhysicsRef(2); + ControlAnchorUpdate first = update(bodyRef, anchorBodyRef, 1.0f); + ControlAnchorUpdate queued = update(bodyRef, anchorBodyRef, 1.0f); + ControlAnchorUpdate afterRelease = update(bodyRef, anchorBodyRef, 1.0f); + + state.trackSubmittedMutation(anchorBodyRef, first); + assertNull(state.selectReadyUpdate(anchorBodyRef, queued)); + + PhysicsKinematicControlSystem.clearMutationState(store, anchorBodyRef); + + assertSame(afterRelease, state.selectReadyUpdate(anchorBodyRef, afterRelease)); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Nonnull + private static ControlAnchorUpdate update(@Nonnull Ref bodyRef, + @Nonnull Ref anchorBodyRef, + float coordinate) { + return new ControlAnchorUpdate(bodyRef, + anchorBodyRef, + new Vector3f(coordinate, coordinate, coordinate), + new Vector3f(coordinate + 1.0f, coordinate + 1.0f, coordinate + 1.0f)); + } + + private static final class TestPhysicsRef extends Ref { + + private TestPhysicsRef(int index) { + super(null, index); + } + + @Override + public boolean isValid() { + return true; + } + } +} diff --git a/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsStoreControlSessionMutationsTest.java b/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsStoreControlSessionMutationsTest.java new file mode 100644 index 00000000..5e3ffeba --- /dev/null +++ b/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/systems/PhysicsStoreControlSessionMutationsTest.java @@ -0,0 +1,443 @@ +package dev.hytalemodding.impulse.builtin.control.internal.systems; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.WorldConfig; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.thread.TickingThread; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.PhysicsAxis; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendJointType; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; +import dev.hytalemodding.impulse.builtin.control.internal.components.PhysicsControlSessionComponent; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendJointHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointType; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsJointEntities; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsSnapshotFrame; +import dev.hytalemodding.impulse.early.PhysicsStoreWorld; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; +import org.objenesis.ObjenesisStd; + +class PhysicsStoreControlSessionMutationsTest { + + private static final ObjenesisStd OBJENESIS = new ObjenesisStd(); + private static final BackendId BACKEND_ID = new BackendId("test:control-release"); + private static final SpaceId SPACE_ID = new SpaceId(42); + + @Test + void releaseClearsTemporarySessionRuntimeAndCopiedState() { + StoreFixture fixture = store("control-release-anchor-cleanup"); + Store physicsStore = fixture.physicsStore(); + Store entityStore = fixture.entityStore(); + try { + markCurrentThreadAsWorldThread(physicsStore); + UUID spaceUuid = uuid(1); + UUID controlledBodyUuid = uuid(2); + UUID anchorBodyUuid = uuid(3); + UUID controlJointUuid = uuid(4); + Ref spaceRef = addSpace(physicsStore, spaceUuid); + BoundSpace space = bindSpace(physicsStore, spaceUuid, spaceRef); + Ref controlledBodyRef = addBody(physicsStore, + spaceUuid, + spaceRef, + controlledBodyUuid, + 0.0f); + Ref anchorBodyRef = addBody(physicsStore, + spaceUuid, + spaceRef, + anchorBodyUuid, + 1.0f); + Ref controlJointRef = addJoint(physicsStore, + spaceRef, + controlledBodyRef, + anchorBodyRef, + controlJointUuid); + publishCopiedState(physicsStore, + spaceUuid, + controlledBodyUuid, + controlledBodyRef, + anchorBodyUuid, + anchorBodyRef); + BackendBodyHandle controlledBodyHandle = bindBody(physicsStore, + space, + controlledBodyUuid, + controlledBodyRef, + 0.0f); + BackendBodyHandle anchorBodyHandle = bindBody(physicsStore, + space, + anchorBodyUuid, + anchorBodyRef, + 1.0f); + bindJoint(physicsStore, + space, + controlJointUuid, + controlJointRef, + anchorBodyHandle, + controlledBodyHandle); + assertEquals(2, space.runtime().bodyCount(space.handle().value())); + assertEquals(1, space.runtime().jointCount(space.handle().value())); + + PhysicsControlSessionComponent session = new PhysicsControlSessionComponent( + controlledBodyRef, + anchorBodyRef, + controlJointRef, + null, + PhysicsBodyType.DYNAMIC, + 4.0f, + new Vector3f(), + new Vector3f()); + + PhysicsStoreControlSessionMutations.applyRelease(entityStore, session); + + PhysicsRuntimeResource runtime = + physicsStore.getResource(PhysicsRuntimeResource.getResourceType()); + assertFalse(anchorBodyRef.isValid()); + assertFalse(controlJointRef.isValid()); + assertNull(physicsStore.getExternalData().getRefFromUUID(anchorBodyUuid)); + assertNull(physicsStore.getExternalData().getRefFromUUID(controlJointUuid)); + assertNull(runtime.getBodyHandle(anchorBodyRef)); + assertNull(runtime.getJointHandle(controlJointRef)); + assertNotNull(runtime.getBodyHandle(controlledBodyRef)); + assertEquals(1, space.runtime().bodyCount(space.handle().value())); + assertEquals(0, space.runtime().jointCount(space.handle().value())); + assertNull(physicsStore.getResource(PhysicsSnapshotResource.getResourceType()) + .getBody(anchorBodyUuid)); + assertFalse(PhysicsBodies.isRegistered(physicsStore, anchorBodyUuid)); + assertNotNull(PhysicsBodies.snapshot(physicsStore, controlledBodyUuid)); + } finally { + fixture.close(); + } + } + + @Nonnull + private static StoreFixture store(@Nonnull String worldName) { + TestWorld world = OBJENESIS.newInstance(TestWorld.class); + setField(world, World.class, "name", worldName); + ComponentRegistry physicsRegistry = new ComponentRegistry<>(); + ComponentRegistryProxy physicsProxy = + new ComponentRegistryProxy<>(new ArrayList<>(), physicsRegistry); + PhysicsComponentTypeRegistry.registerComponentTypes(physicsProxy); + PhysicsResourceTypes.registerResourceTypes(physicsProxy); + PhysicsStore physicsStoreExternal = new PhysicsStore(world); + Store physicsStore = + physicsRegistry.addStore(physicsStoreExternal, EmptyResourceStorage.get()); + setField(physicsStoreExternal, PhysicsStore.class, "store", physicsStore); + world.physicsStore = physicsStoreExternal; + + ComponentRegistry entityRegistry = new ComponentRegistry<>(); + Store entityStore = entityRegistry.addStore(new EntityStore(world), + EmptyResourceStorage.get()); + return new StoreFixture(physicsRegistry, entityRegistry, physicsStore, entityStore); + } + + @Nonnull + private static Ref addSpace(@Nonnull Store store, + @Nonnull UUID spaceUuid) { + Ref ref = store.addEntity(PhysicsEntities.spaceHolder(store, + spaceUuid, + new SpaceComponent(BACKEND_ID, new Vector3f(0.0f, -9.81f, 0.0f))), + AddReason.SPAWN); + assertNotNull(ref); + store.getExternalData().putRefForUUID(spaceUuid, ref); + store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .putSpace(SPACE_ID, spaceUuid); + return ref; + } + + @Nonnull + private static BoundSpace bindSpace(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull Ref spaceRef) { + PhysicsBackendRuntime backendRuntime = + new FakePhysicsBackendRuntimeProvider(BACKEND_ID, false, false).createRuntime(); + BackendSpaceHandle spaceHandle = + new BackendSpaceHandle(backendRuntime.createSpace(SPACE_ID)); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + runtime.putRuntime(BACKEND_ID, backendRuntime); + runtime.putSpaceHandle(spaceRef, BACKEND_ID, spaceHandle); + runtime.putSpaceMetadata(BACKEND_ID, spaceHandle, spaceUuid, spaceRef); + return new BoundSpace(spaceUuid, spaceRef, backendRuntime, spaceHandle); + } + + @Nonnull + private static Ref addBody(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull Ref spaceRef, + @Nonnull UUID bodyUuid, + float positionX) { + BodyComponent body = new BodyComponent(spaceUuid); + body.setSpaceRef(spaceRef); + Ref ref = store.addEntity(PhysicsEntities.bodyHolder(store, + bodyUuid, + body, + new DynamicsComponent(PhysicsBodyType.DYNAMIC, 1.0f, 0.0f, 0.0f, false), + null, + new ColliderComponent(new Vector3f(), new Quaternionf(), false), + new ShapeComponent(ShapeType.BOX, + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + PhysicsAxis.Y, + 0.0f, + ""), + new MaterialComponent(0.5f, 0.1f), + new CollisionFilterComponent(0x01, 0x02)), + AddReason.SPAWN); + assertNotNull(ref); + store.getExternalData().putRefForUUID(bodyUuid, ref); + return ref; + } + + @Nonnull + private static Ref addJoint(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull Ref controlledBodyRef, + @Nonnull Ref anchorBodyRef, + @Nonnull UUID jointUuid) { + JointComponent joint = PhysicsJointEntities.joint(spaceRef, + anchorBodyRef, + controlledBodyRef, + JointType.POINT, + new Vector3f(), + new Vector3f(), + new Vector3f()); + Ref ref = store.addEntity(PhysicsEntities.jointHolder(store, + jointUuid, + joint), + AddReason.SPAWN); + assertNotNull(ref); + store.getExternalData().putRefForUUID(jointUuid, ref); + return ref; + } + + @Nonnull + private static BackendBodyHandle bindBody(@Nonnull Store store, + @Nonnull BoundSpace space, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef, + float positionX) { + long bodyId = space.runtime().createBody(space.handle().value(), + BackendRuntimeCodes.shapeTypeCode(ShapeType.BOX), + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + BackendRuntimeCodes.axisCode(PhysicsAxis.Y), + 0.0f, + 1.0f, + BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC), + positionX, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + BackendBodyHandle handle = new BackendBodyHandle(bodyId); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + runtime.putBodyHandle(bodyRef, space.ref(), space.handle(), handle); + runtime.putBodySnapshotMetadata(BACKEND_ID, + space.handle(), + handle, + bodyUuid, + bodyRef, + space.uuid()); + runtime.putBodyHitMetadata(BACKEND_ID, + space.handle(), + handle, + bodyUuid, + bodyRef, + PhysicsBodyType.DYNAMIC, + ShapeType.BOX); + return handle; + } + + private static void bindJoint(@Nonnull Store store, + @Nonnull BoundSpace space, + @Nonnull UUID jointUuid, + @Nonnull Ref jointRef, + @Nonnull BackendBodyHandle bodyAHandle, + @Nonnull BackendBodyHandle bodyBHandle) { + long jointId = space.runtime().createJoint(space.handle().value(), + BackendRuntimeCodes.jointTypeCode(BackendJointType.POINT), + bodyAHandle.value(), + bodyBHandle.value(), + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + false, + 0.0f, + 0.0f); + BackendJointHandle handle = new BackendJointHandle(jointId); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + runtime.putJointHandle(jointRef, space.ref(), space.handle(), handle); + runtime.putJointMetadata(BACKEND_ID, space.handle(), handle, jointUuid, jointRef); + } + + private static void publishCopiedState(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull UUID controlledBodyUuid, + @Nonnull Ref controlledBodyRef, + @Nonnull UUID anchorBodyUuid, + @Nonnull Ref anchorBodyRef) { + store.getResource(PhysicsSnapshotResource.getResourceType()) + .publish(new PhysicsSnapshotFrame(1L, + 0.05f, + List.of(snapshot(controlledBodyRef, controlledBodyUuid, spaceUuid, 0.0f), + snapshot(anchorBodyRef, anchorBodyUuid, spaceUuid, 1.0f)))); + } + + @Nonnull + private static PhysicsBodySnapshot snapshot(@Nonnull Ref bodyRef, + @Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid, + float positionX) { + return PhysicsBodySnapshot.of(bodyRef, + bodyUuid, + spaceUuid, + PhysicsBodyType.DYNAMIC, + positionX, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + false); + } + + @Nonnull + private static UUID uuid(long leastSignificantBits) { + return new UUID(0L, leastSignificantBits); + } + + private static void markCurrentThreadAsWorldThread(@Nonnull Store store) { + try { + Method setThread = TickingThread.class.getDeclaredMethod("setThread", Thread.class); + setThread.setAccessible(true); + setThread.invoke(store.getExternalData().getWorld(), Thread.currentThread()); + } catch (NoSuchMethodException | IllegalAccessException exception) { + throw new AssertionError("Could not mark test world thread", exception); + } catch (InvocationTargetException exception) { + throw new AssertionError("Could not mark test world thread", + exception.getTargetException()); + } + } + + private static void setField(@Nonnull Object target, + @Nonnull Class owner, + @Nonnull String name, + Object value) { + try { + Field field = owner.getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } catch (ReflectiveOperationException exception) { + throw new AssertionError("Failed to set test field " + owner.getName() + "." + name, + exception); + } + } + + private static final class TestWorld extends World implements PhysicsStoreWorld { + + private PhysicsStore physicsStore; + + private TestWorld() throws IOException { + super("unused", Path.of("build/tmp/unused-control-release-world"), (WorldConfig) null); + } + + @Nonnull + @Override + public PhysicsStore getPhysicsStore() { + return physicsStore; + } + } + + private record BoundSpace( + @Nonnull UUID uuid, + @Nonnull Ref ref, + @Nonnull PhysicsBackendRuntime runtime, + @Nonnull BackendSpaceHandle handle) { + } + + private record StoreFixture( + @Nonnull ComponentRegistry physicsRegistry, + @Nonnull ComponentRegistry entityRegistry, + @Nonnull Store physicsStore, + @Nonnull Store entityStore) implements AutoCloseable { + + @Override + public void close() { + physicsRegistry.removeStore(physicsStore); + entityRegistry.removeStore(entityStore); + physicsRegistry.shutdown(); + entityRegistry.shutdown(); + } + } +} diff --git a/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/testsupport/TestInstanceFactory.java b/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/testsupport/TestInstanceFactory.java new file mode 100644 index 00000000..81c642d7 --- /dev/null +++ b/impulse-builtins/control/src/test/java/dev/hytalemodding/impulse/builtin/control/internal/testsupport/TestInstanceFactory.java @@ -0,0 +1,70 @@ +package dev.hytalemodding.impulse.builtin.control.internal.testsupport; + +import com.hypixel.hytale.server.core.universe.world.World; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.objenesis.ObjenesisStd; + +/** + * Allocates Hytale server classes whose public constructors bind to global server state. + */ +public final class TestInstanceFactory { + + private static final ObjenesisStd OBJENESIS = new ObjenesisStd(); + + private TestInstanceFactory() { + } + + @Nonnull + public static World world(@Nonnull String worldName) { + World world = allocate(World.class); + setField(world, World.class, "name", worldName); + return world; + } + + @Nonnull + private static T allocate(@Nonnull Class type) { + T constructed = constructNoArg(type); + if (constructed != null) { + return constructed; + } + return constructWithoutInvokingConstructor(type); + } + + @Nullable + private static T constructNoArg(@Nonnull Class type) { + try { + Constructor constructor = type.getDeclaredConstructor(); + constructor.setAccessible(true); + return constructor.newInstance(); + } catch (NoSuchMethodException exception) { + return null; + } catch (ReflectiveOperationException exception) { + throw new AssertionError("Failed to allocate test instance of " + type.getName(), exception); + } + } + + @Nonnull + private static T constructWithoutInvokingConstructor(@Nonnull Class type) { + try { + return OBJENESIS.newInstance(type); + } catch (RuntimeException exception) { + throw new AssertionError("Failed to allocate test instance of " + type.getName(), exception); + } + } + + private static void setField(@Nonnull Object target, + @Nonnull Class owner, + @Nonnull String name, + @Nonnull Object value) { + try { + Field field = owner.getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } catch (ReflectiveOperationException exception) { + throw new AssertionError("Failed to set test field " + owner.getName() + "." + name, exception); + } + } +} diff --git a/impulse-bullet/build.gradle.kts b/impulse-bullet/build.gradle.kts deleted file mode 100644 index 2fecc8d1..00000000 --- a/impulse-bullet/build.gradle.kts +++ /dev/null @@ -1,131 +0,0 @@ -import org.gradle.api.file.FileCollection -import org.gradle.api.file.DuplicatesStrategy -import org.gradle.jvm.tasks.Jar -import java.io.File - -plugins { - id("java-library") -} - -data class BulletBackendPlatform( - val taskSuffix: String, - val archiveClassifier: String, - val nativeArtifact: String -) - -val bulletBackendPlatforms = listOf( - BulletBackendPlatform("LinuxX64", "linux-x86_64", "Libbulletjme-Linux64"), - BulletBackendPlatform("LinuxArm64", "linux-arm64", "Libbulletjme-Linux_ARM64"), - BulletBackendPlatform("OsxArm64", "osx-arm64", "Libbulletjme-MacOSX_ARM64"), - BulletBackendPlatform("WindowsX64", "windows-x86_64", "Libbulletjme-Windows64") -) -val impulseLicenseFile = rootProject.layout.projectDirectory.file("LICENSE") -val libbulletjmeLicenseFile = rootProject.layout.projectDirectory.file("licenses/LIBBULLETJME_LICENSE") - -dependencies { - api(project(":impulse-api")) - - implementation(project(":impulse-native-loader")) - implementation(libs.libbulletjme) - runtimeOnly(variantOf(libs.libbulletjme.native) { classifier("SpRelease") }) - - compileOnly(libs.lombok) - annotationProcessor(libs.lombok) - -} - -val bulletNativeConfigurations = bulletBackendPlatforms.associateWith { platform -> - configurations.create("bulletNative${platform.taskSuffix}") { - isCanBeConsumed = false - isCanBeResolved = true - isTransitive = false - } -} - -bulletBackendPlatforms.forEach { platform -> - dependencies.add(bulletNativeConfigurations.getValue(platform).name, - "com.github.stephengold:${platform.nativeArtifact}:${libs.versions.libbulletjme.get()}:SpRelease") -} - -fun runtimeClasspathWithoutBundledApi(): FileCollection { - return configurations.runtimeClasspath.get() - .filter { file -> !file.name.startsWith("impulse-api-") } -} - -fun runtimeClasspathWithoutBundledApiAndHostNative(): FileCollection { - return runtimeClasspathWithoutBundledApi() - .filter { file -> !isBulletNativeJar(file) } -} - -fun isBulletNativeJar(file: File): Boolean { - return file.name.startsWith("Libbulletjme-") && file.name.contains("-Sp") -} - -fun Jar.expandRuntimeClasspath(runtimeClasspath: FileCollection) { - dependsOn(runtimeClasspath.buildDependencies) - from({ - runtimeClasspath.map { file -> if (file.isDirectory) file else zipTree(file) } - }) -} - -fun Jar.includeBackendRuntime() { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - expandRuntimeClasspath(runtimeClasspathWithoutBundledApiAndHostNative()) - exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA") -} - -fun Jar.includeBackendLicenseNotices() { - from(impulseLicenseFile) { - into("META-INF/licenses/impulse") - rename { "LICENSE" } - } - from(libbulletjmeLicenseFile) { - into("META-INF/licenses/libbulletjme") - rename { "LICENSE" } - } -} - -tasks.jar { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - expandRuntimeClasspath(runtimeClasspathWithoutBundledApi()) - includeBackendLicenseNotices() - exclude("META-INF/*.SF", "META-INF/*.DSA", "META-INF/*.RSA") -} - -val platformJarTasks = bulletBackendPlatforms.map { platform -> - tasks.register("packageBulletBackend${platform.taskSuffix}") { - group = "build" - description = "Packages the Bullet backend provider jar for ${platform.archiveClassifier}" - archiveClassifier.set(platform.archiveClassifier) - - from(sourceSets.main.get().output) - includeBackendRuntime() - includeBackendLicenseNotices() - from({ - bulletNativeConfigurations.getValue(platform).files - .map { file -> if (file.isDirectory) file else zipTree(file) } - }) - } -} - -tasks.register("packageBulletBackendUniversal") { - group = "build" - description = "Packages the Bullet backend provider jar with every configured native library" - archiveClassifier.set("universal") - - from(sourceSets.main.get().output) - includeBackendRuntime() - includeBackendLicenseNotices() - from({ - bulletNativeConfigurations.values - .flatMap { configuration -> configuration.files } - .map { file -> if (file.isDirectory) file else zipTree(file) } - }) -} - -tasks.register("packageBulletBackendPlatformJars") { - group = "build" - description = "Packages all Bullet per-platform backend jars plus the universal jar" - dependsOn(platformJarTasks) - dependsOn(tasks.named("packageBulletBackendUniversal")) -} diff --git a/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletBackend.java b/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletBackend.java deleted file mode 100644 index 5fd4623e..00000000 --- a/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletBackend.java +++ /dev/null @@ -1,106 +0,0 @@ -package dev.hytalemodding.impulse.bullet; - -import com.jme3.bullet.util.NativeLibrary; -import com.jme3.bullet.PhysicsSpace.BroadphaseType; -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.PhysicsBackend; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.internal.nativelib.NativeLibraryLoader; -import dev.hytalemodding.impulse.internal.nativelib.NativeLibraryResource; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.annotation.Nonnull; - -/** - * TODO: right now the BulletBackend has an additional bulletjme layer on top of Bullet natives - */ -public final class BulletBackend implements PhysicsBackend { - - public static final BackendId ID = new BackendId("impulse:bullet"); - private static final Logger LOGGER = Logger.getLogger("Impulse"); - private static final AtomicInteger SPACES_CREATED = new AtomicInteger(0); - private static final Object NATIVE_LOAD_LOCK = new Object(); - private static volatile boolean nativeLibraryLoaded; - - private volatile Level internalLoggingLevel = Level.WARNING; - - private volatile boolean initialized; - - @Nonnull - @Override - public BackendId getId() { - return ID; - } - - @Override - public void setInternalLoggingLevel(@Nonnull Level level) { - this.internalLoggingLevel = level; - if (initialized) { - applyInternalLoggingLevel(level); - } - } - - private void applyInternalLoggingLevel(@Nonnull Level level) { - // Set on both parent and the specific PhysicsRigidBody logger, - // since child loggers can have their own level independent of parent. - Logger.getLogger("com.jme3.bullet").setLevel(level); - Logger.getLogger("com.jme3.bullet.objects.PhysicsRigidBody").setLevel(level); - } - - @Override - public synchronized void init() { - if (initialized) { - return; - } - - loadNativeLibrary(); - NativeLibrary.setStartupMessageEnabled(false); - - initialized = true; - applyInternalLoggingLevel(internalLoggingLevel); - LOGGER.log(Level.INFO, "Bullet backend initialized"); - } - - private static void loadNativeLibrary() { - if (nativeLibraryLoaded) { - return; - } - - synchronized (NATIVE_LOAD_LOCK) { - if (nativeLibraryLoaded) { - return; - } - - try { - NativeLibraryLoader.load(BulletBackend.class, - "bullet", - NativeLibraryResource.forCurrentPlatform("bulletjme")); - } catch (IllegalArgumentException | IllegalStateException exception) { - throw new IllegalStateException("Failed to load the Libbulletjme native library", - exception); - } - - nativeLibraryLoaded = true; - } - } - - @Nonnull - @Override - public PhysicsSpace createSpace() { - return createSpace(SpaceId.next()); - } - - @Nonnull - @Override - public PhysicsSpace createSpace(@Nonnull SpaceId spaceId) { - int count = SPACES_CREATED.incrementAndGet(); - LOGGER.log(Level.FINE, - "Creating Bullet physics space #" + count + " on thread " - + Thread.currentThread().getName()); - - return new BulletSpace(spaceId, this, - new BulletNativeSpace(BroadphaseType.DBVT)); - } -} diff --git a/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletBody.java b/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletBody.java deleted file mode 100644 index b4ff6340..00000000 --- a/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletBody.java +++ /dev/null @@ -1,578 +0,0 @@ -package dev.hytalemodding.impulse.bullet; - -import com.jme3.bullet.collision.shapes.BoxCollisionShape; -import com.jme3.bullet.collision.shapes.CapsuleCollisionShape; -import com.jme3.bullet.collision.shapes.CollisionShape; -import com.jme3.bullet.collision.shapes.ConeCollisionShape; -import com.jme3.bullet.collision.shapes.CylinderCollisionShape; -import com.jme3.bullet.collision.shapes.PlaneCollisionShape; -import com.jme3.bullet.collision.shapes.SphereCollisionShape; -import com.jme3.bullet.objects.PhysicsRigidBody; -import com.jme3.math.Quaternion; -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.ShapeType; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -public final class BulletBody implements PhysicsBody { - - private final PhysicsRigidBody body; - private final com.jme3.math.Vector3f jmeVectorScratch = new com.jme3.math.Vector3f(); - private final Quaternion jmeQuaternionScratch = new Quaternion(); - private BulletSpace owner; - private boolean attachedToSpace; - private boolean invalidated; - - BulletBody(@Nonnull PhysicsRigidBody body) { - this.body = body; - } - - @Override - public void setPosition(float x, float y, float z) { - body.setPhysicsLocation(toJme(x, y, z)); - } - - @Override - public void setPosition(@Nonnull Vector3f pos) { - setPosition(pos.x, pos.y, pos.z); - } - - @Nonnull - @Override - public Vector3f getPosition() { - Vector3f out = new Vector3f(); - getPosition(out); - return out; - } - - @Override - public void getPosition(@Nonnull Vector3f out) { - com.jme3.math.Vector3f position = body.getPhysicsLocation(jmeVectorScratch); - out.set(position.x, position.y, position.z); - } - - @Override - public void setRotation(float x, float y, float z, float w) { - body.setPhysicsRotation(new Quaternion(x, y, z, w)); - } - - @Override - public void setRotation(@Nonnull Quaternionf rot) { - setRotation(rot.x, rot.y, rot.z, rot.w); - } - - @Nonnull - @Override - public Quaternionf getRotation() { - Quaternionf out = new Quaternionf(); - getRotation(out); - return out; - } - - @Override - public void getRotation(@Nonnull Quaternionf out) { - Quaternion rotation = body.getPhysicsRotation(jmeQuaternionScratch); - out.set(rotation.getX(), rotation.getY(), rotation.getZ(), rotation.getW()); - } - - @Override - public void setRestitution(float restitution) { - body.setRestitution(restitution); - } - - @Override - public float getRestitution() { - return body.getRestitution(); - } - - @Override - public void setFriction(float friction) { - body.setFriction(friction); - } - - @Override - public float getFriction() { - return body.getFriction(); - } - - @Nonnull - @Override - public PhysicsBodyType getBodyType() { - if (body.isStatic()) { - return PhysicsBodyType.STATIC; - } - if (body.isKinematic()) { - return PhysicsBodyType.KINEMATIC; - } - return PhysicsBodyType.DYNAMIC; - } - - @Override - public void setBodyType(@Nonnull PhysicsBodyType bodyType) { - switch (bodyType) { - case STATIC -> { - body.setKinematic(false); - body.setMass(com.jme3.bullet.objects.PhysicsBody.massForStatic); - } - case DYNAMIC -> { - if (body.getMass() <= 0f) { - body.setMass(1f); - } - body.setKinematic(false); - } - case KINEMATIC -> { - if (body.getMass() <= 0f) { - body.setMass(1f); - } - body.setKinematic(true); - } - } - } - - @Override - public boolean isStatic() { - return body.isStatic(); - } - - @Override - public boolean isKinematic() { - return body.isKinematic(); - } - - @Override - public void setKinematic(boolean kinematic) { - if (kinematic) { - setBodyType(PhysicsBodyType.KINEMATIC); - } else if (!body.isStatic()) { - body.setKinematic(false); - } - } - - @Override - public void activate() { - body.activate(); - } - - @Override - public boolean isActive() { - return body.isActive(); - } - - @Override - public boolean isSleeping() { - return !body.isActive(); - } - - @Override - public void sleep() { - body.setLinearVelocity(toJme(0f, 0f, 0f)); - body.setAngularVelocity(toJme(0f, 0f, 0f)); - body.setEnableSleep(true); - } - - @Override - public float getMass() { - return body.getMass(); - } - - @Override - public void setMass(float mass) { - body.setMass(mass); - } - - @Nonnull - @Override - public Vector3f getLinearVelocity() { - Vector3f out = new Vector3f(); - getLinearVelocity(out); - return out; - } - - @Override - public void getLinearVelocity(@Nonnull Vector3f out) { - com.jme3.math.Vector3f velocity = body.getLinearVelocity(jmeVectorScratch); - out.set(velocity.x, velocity.y, velocity.z); - } - - @Override - public void setLinearVelocity(@Nonnull Vector3f vel) { - setLinearVelocity(vel.x, vel.y, vel.z); - } - - @Override - public void setLinearVelocity(float x, float y, float z) { - body.setLinearVelocity(toJme(x, y, z)); - } - - @Nonnull - @Override - public Vector3f getAngularVelocity() { - Vector3f out = new Vector3f(); - getAngularVelocity(out); - return out; - } - - @Override - public void getAngularVelocity(@Nonnull Vector3f out) { - com.jme3.math.Vector3f velocity = body.getAngularVelocity(jmeVectorScratch); - out.set(velocity.x, velocity.y, velocity.z); - } - - @Override - public void setAngularVelocity(@Nonnull Vector3f vel) { - setAngularVelocity(vel.x, vel.y, vel.z); - } - - @Override - public void setAngularVelocity(float x, float y, float z) { - body.setAngularVelocity(toJme(x, y, z)); - } - - @Override - public float getLinearDamping() { - return body.getLinearDamping(); - } - - @Override - public void setLinearDamping(float damping) { - body.setLinearDamping(damping); - } - - @Override - public float getAngularDamping() { - return body.getAngularDamping(); - } - - @Override - public void setAngularDamping(float damping) { - body.setAngularDamping(damping); - } - - @Override - public void applyCentralForce(@Nonnull Vector3f force) { - applyCentralForce(force.x, force.y, force.z); - } - - @Override - public void applyCentralForce(float x, float y, float z) { - body.applyCentralForce(toJme(x, y, z)); - } - - @Override - public void applyForce(@Nonnull Vector3f force, @Nonnull Vector3f offset) { - applyForce(force.x, force.y, force.z, offset.x, offset.y, offset.z); - } - - @Override - public void applyForce(float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ) { - body.applyForce(toJme(x, y, z), toJme(offsetX, offsetY, offsetZ)); - } - - @Override - public void applyCentralImpulse(@Nonnull Vector3f impulse) { - applyCentralImpulse(impulse.x, impulse.y, impulse.z); - } - - @Override - public void applyCentralImpulse(float x, float y, float z) { - body.applyCentralImpulse(toJme(x, y, z)); - } - - @Override - public void applyImpulse(@Nonnull Vector3f impulse, @Nonnull Vector3f offset) { - applyImpulse(impulse.x, impulse.y, impulse.z, offset.x, offset.y, offset.z); - } - - @Override - public void applyImpulse(float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ) { - body.applyImpulse(toJme(x, y, z), toJme(offsetX, offsetY, offsetZ)); - } - - @Override - public void applyTorque(@Nonnull Vector3f torque) { - applyTorque(torque.x, torque.y, torque.z); - } - - @Override - public void applyTorque(float x, float y, float z) { - body.applyTorque(toJme(x, y, z)); - } - - @Override - public void applyTorqueImpulse(@Nonnull Vector3f torqueImpulse) { - applyTorqueImpulse(torqueImpulse.x, torqueImpulse.y, torqueImpulse.z); - } - - @Override - public void applyTorqueImpulse(float x, float y, float z) { - body.applyTorqueImpulse(toJme(x, y, z)); - } - - @Override - public void clearForces() { - body.clearForces(); - } - - @Override - public boolean isSensor() { - return !body.isContactResponse(); - } - - @Override - public void setSensor(boolean sensor) { - body.setContactResponse(!sensor); - } - - @Override - public int getCollisionGroup() { - return body.getCollisionGroup(); - } - - @Override - public int getCollisionMask() { - return body.getCollideWithGroups(); - } - - @Override - public void setCollisionFilter(int group, int mask) { - body.setCollisionGroup(group); - body.setCollideWithGroups(mask); - } - - @Override - public boolean isContinuousCollisionEnabled() { - return body.getCcdMotionThreshold() > 0f; - } - - @Override - public void setContinuousCollisionEnabled(boolean enabled) { - if (enabled) { - body.setCcdMotionThreshold(0.0001f); - body.setCcdSweptSphereRadius(Math.max(0.001f, estimateCcdRadius())); - } else { - body.setCcdMotionThreshold(0f); - body.setCcdSweptSphereRadius(0f); - } - } - - @Nonnull - @Override - public ShapeType getShapeType() { - CollisionShape shape = body.getCollisionShape(); - if (shape instanceof BoxCollisionShape) { - return ShapeType.BOX; - } - if (shape instanceof SphereCollisionShape) { - return ShapeType.SPHERE; - } - if (shape instanceof CapsuleCollisionShape) { - return ShapeType.CAPSULE; - } - if (shape instanceof CylinderCollisionShape) { - return ShapeType.CYLINDER; - } - if (shape instanceof ConeCollisionShape) { - return ShapeType.CONE; - } - if (shape instanceof PlaneCollisionShape) { - return ShapeType.PLANE; - } - return ShapeType.UNKNOWN; - } - - @Nullable - @Override - public Vector3f getBoxHalfExtents() { - CollisionShape shape = body.getCollisionShape(); - if (shape instanceof BoxCollisionShape box) { - return fromJme(box.getHalfExtents(new com.jme3.math.Vector3f())); - } - if (shape instanceof CylinderCollisionShape cylinder) { - return fromJme(cylinder.getHalfExtents(new com.jme3.math.Vector3f())); - } - return null; - } - - @Override - public float getSphereRadius() { - CollisionShape shape = body.getCollisionShape(); - if (shape instanceof SphereCollisionShape sphere) { - return sphere.getRadius(); - } - if (shape instanceof CapsuleCollisionShape capsule) { - return capsule.getRadius(); - } - if (shape instanceof CylinderCollisionShape cylinder) { - com.jme3.math.Vector3f halfExtents = cylinder.getHalfExtents( - new com.jme3.math.Vector3f()); - return switch (cylinder.getAxis()) { - case 0 -> Math.max(halfExtents.y, halfExtents.z); - case 1 -> Math.max(halfExtents.x, halfExtents.z); - case 2 -> Math.max(halfExtents.x, halfExtents.y); - default -> Math.max(halfExtents.x, Math.max(halfExtents.y, halfExtents.z)); - }; - } - if (shape instanceof ConeCollisionShape cone) { - return cone.getRadius(); - } - return -1f; - } - - @Override - public float getHalfHeight() { - CollisionShape shape = body.getCollisionShape(); - if (shape instanceof CapsuleCollisionShape capsule) { - return capsule.getHeight() * 0.5f; - } - if (shape instanceof CylinderCollisionShape cylinder) { - return cylinder.getHeight() * 0.5f; - } - if (shape instanceof ConeCollisionShape cone) { - return cone.getHeight() * 0.5f; - } - return -1f; - } - - @Nonnull - @Override - public PhysicsAxis getShapeAxis() { - CollisionShape shape = body.getCollisionShape(); - if (shape instanceof CapsuleCollisionShape capsule) { - return PhysicsAxis.fromIndex(capsule.getAxis()); - } - if (shape instanceof CylinderCollisionShape cylinder) { - return PhysicsAxis.fromIndex(cylinder.getAxis()); - } - if (shape instanceof ConeCollisionShape cone) { - return PhysicsAxis.fromIndex(cone.getAxis()); - } - return PhysicsAxis.Y; - } - - @Override - public float getCenterOfMassOffsetY() { - CollisionShape shape = body.getCollisionShape(); - if (shape instanceof BoxCollisionShape box) { - return box.getHalfExtents(new com.jme3.math.Vector3f()).y; - } - if (shape instanceof SphereCollisionShape sphere) { - return sphere.getRadius(); - } - if (shape instanceof CapsuleCollisionShape capsule) { - return capsule.getAxis() == PhysicsAxis.Y.index() - ? capsule.getHeight() * 0.5f + capsule.getRadius() - : capsule.getRadius(); - } - if (shape instanceof CylinderCollisionShape cylinder) { - return cylinder.getAxis() == PhysicsAxis.Y.index() - ? cylinder.getHeight() * 0.5f - : cylinder.maxRadius(); - } - if (shape instanceof ConeCollisionShape cone) { - return cone.getAxis() == PhysicsAxis.Y.index() - ? cone.getHeight() * 0.5f - : cone.maxRadius(); - } - return 0f; - } - - @Nonnull - PhysicsRigidBody getRigidBody() { - return body; - } - - long getNativeId() { - return body.nativeId(); - } - - @Nullable - BulletSpace getOwner() { - return owner; - } - - boolean isOwnedBy(@Nonnull BulletSpace space) { - return owner == space; - } - - boolean isAttachedToSpace() { - return attachedToSpace; - } - - boolean isInvalidated() { - return invalidated; - } - - void bindTo(@Nonnull BulletSpace space) { - requireNotInvalidated(); - if (owner != null && owner != space) { - throw new IllegalStateException("Body belongs to another bullet space"); - } - owner = space; - } - - void markAttachedTo(@Nonnull BulletSpace space) { - bindTo(space); - if (attachedToSpace) { - throw new IllegalStateException("Body is already attached to a bullet space"); - } - attachedToSpace = true; - } - - void detachFrom(@Nonnull BulletSpace space) { - if (owner == space) { - attachedToSpace = false; - owner = null; - } - } - - void invalidateFrom(@Nonnull BulletSpace space) { - if (owner == space) { - attachedToSpace = false; - owner = null; - invalidated = true; - } - } - - void requireNotInvalidated() { - if (invalidated) { - throw new IllegalStateException("Body has been invalidated"); - } - } - - private float estimateCcdRadius() { - return switch (getShapeType()) { - case BOX, CYLINDER -> { - Vector3f half = getBoxHalfExtents(); - yield half != null ? Math.min(half.x, Math.min(half.y, half.z)) : 0.05f; - } - case SPHERE, CAPSULE, CONE -> Math.max(0.001f, getSphereRadius()); - default -> 0.05f; - }; - } - - private static com.jme3.math.Vector3f toJme(@Nonnull Vector3f vector) { - return toJme(vector.x, vector.y, vector.z); - } - - private static com.jme3.math.Vector3f toJme(float x, float y, float z) { - return new com.jme3.math.Vector3f(x, y, z); - } - - private static Vector3f fromJme(@Nonnull com.jme3.math.Vector3f vector) { - return new Vector3f(vector.x, vector.y, vector.z); - } -} diff --git a/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletJoint.java b/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletJoint.java deleted file mode 100644 index 6cb9553c..00000000 --- a/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletJoint.java +++ /dev/null @@ -1,190 +0,0 @@ -package dev.hytalemodding.impulse.bullet; - -import com.jme3.bullet.joints.Constraint; -import com.jme3.bullet.joints.HingeJoint; -import com.jme3.bullet.joints.SliderJoint; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsJoint; -import dev.hytalemodding.impulse.api.PhysicsJointType; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3f; - -final class BulletJoint implements PhysicsJoint { - - private final PhysicsJointType type; - private final BulletBody bodyA; - private final BulletBody bodyB; - private final com.jme3.bullet.joints.PhysicsJoint joint; - private final Vector3f anchorA; - private final Vector3f anchorB; - private final Vector3f axis; - private float lowerLimit; - private float upperLimit; - private boolean motorEnabled; - private float motorTargetVelocity; - private float motorMaxForce; - private final float springRestLength; - private final float springStiffness; - private final float springDamping; - - BulletJoint(@Nonnull PhysicsJointType type, - @Nonnull BulletBody bodyA, - @Nonnull BulletBody bodyB, - @Nonnull com.jme3.bullet.joints.PhysicsJoint joint, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nullable Vector3f axis) { - this(type, bodyA, bodyB, joint, anchorA, anchorB, axis, - Float.NaN, Float.NaN, Float.NaN); - } - - BulletJoint(@Nonnull PhysicsJointType type, - @Nonnull BulletBody bodyA, - @Nonnull BulletBody bodyB, - @Nonnull com.jme3.bullet.joints.PhysicsJoint joint, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nullable Vector3f axis, - float springRestLength, - float springStiffness, - float springDamping) { - this.type = type; - this.bodyA = bodyA; - this.bodyB = bodyB; - this.joint = joint; - this.anchorA = new Vector3f(anchorA); - this.anchorB = new Vector3f(anchorB); - this.axis = axis != null ? new Vector3f(axis) : null; - this.springRestLength = springRestLength; - this.springStiffness = springStiffness; - this.springDamping = springDamping; - } - - @Nonnull - @Override - public PhysicsJointType getType() { - return type; - } - - @Nonnull - @Override - public PhysicsBody getBodyA() { - return bodyA; - } - - @Nonnull - @Override - public PhysicsBody getBodyB() { - return bodyB; - } - - @Override - public boolean isEnabled() { - return joint.isEnabled(); - } - - @Override - public void setEnabled(boolean enabled) { - if (joint instanceof Constraint constraint) { - constraint.setEnabled(enabled); - } - } - - @Nonnull - @Override - public Vector3f getAnchorA() { - return new Vector3f(anchorA); - } - - @Nonnull - @Override - public Vector3f getAnchorB() { - return new Vector3f(anchorB); - } - - @Nullable - @Override - public Vector3f getAxis() { - return axis != null ? new Vector3f(axis) : null; - } - - @Override - public float getLowerLimit() { - return lowerLimit; - } - - @Override - public float getUpperLimit() { - return upperLimit; - } - - @Override - public void setLimits(float lowerLimit, float upperLimit) { - this.lowerLimit = lowerLimit; - this.upperLimit = upperLimit; - if (joint instanceof HingeJoint hinge) { - hinge.setLimit(lowerLimit, upperLimit); - } else if (joint instanceof SliderJoint slider) { - slider.setLowerLinLimit(lowerLimit); - slider.setUpperLinLimit(upperLimit); - } - } - - @Override - public boolean isMotorEnabled() { - return motorEnabled; - } - - @Override - public void setMotorEnabled(boolean enabled) { - motorEnabled = enabled; - if (joint instanceof HingeJoint hinge) { - hinge.enableMotor(enabled, motorTargetVelocity, motorMaxForce); - } else if (joint instanceof SliderJoint slider) { - slider.setPoweredLinMotor(enabled); - } - } - - @Override - public float getMotorTargetVelocity() { - return motorTargetVelocity; - } - - @Override - public float getMotorMaxForce() { - return motorMaxForce; - } - - @Override - public void setMotor(float targetVelocity, float maxForce) { - motorTargetVelocity = targetVelocity; - motorMaxForce = maxForce; - if (joint instanceof HingeJoint hinge) { - hinge.enableMotor(motorEnabled, targetVelocity, maxForce); - } else if (joint instanceof SliderJoint slider) { - slider.setTargetLinMotorVelocity(targetVelocity); - slider.setMaxLinMotorForce(maxForce); - } - } - - @Override - public float getSpringRestLength() { - return springRestLength; - } - - @Override - public float getSpringStiffness() { - return springStiffness; - } - - @Override - public float getSpringDamping() { - return springDamping; - } - - @Nonnull - com.jme3.bullet.joints.PhysicsJoint getNativeJoint() { - return joint; - } -} diff --git a/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletNativeContactEvent.java b/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletNativeContactEvent.java deleted file mode 100644 index 386cb282..00000000 --- a/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletNativeContactEvent.java +++ /dev/null @@ -1,53 +0,0 @@ -package dev.hytalemodding.impulse.bullet; - -import dev.hytalemodding.impulse.api.PhysicsContactPhase; -import java.util.Objects; -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -record BulletNativeContactEvent(@Nonnull PhysicsContactPhase phase, - long bodyAId, - long bodyBId, - @Nonnull Vector3f pointOnA, - @Nonnull Vector3f pointOnB, - @Nonnull Vector3f normalOnB, - float distance, - float impulse) { - - BulletNativeContactEvent { - phase = Objects.requireNonNull(phase, "phase"); - pointOnA = new Vector3f(Objects.requireNonNull(pointOnA, "pointOnA")); - pointOnB = new Vector3f(Objects.requireNonNull(pointOnB, "pointOnB")); - normalOnB = new Vector3f(Objects.requireNonNull(normalOnB, "normalOnB")); - } - - @Nonnull - BulletNativeContactEvent withPhase(@Nonnull PhysicsContactPhase phase) { - return new BulletNativeContactEvent(phase, - bodyAId, - bodyBId, - pointOnA, - pointOnB, - normalOnB, - distance, - impulse); - } - - @Nonnull - @Override - public Vector3f pointOnA() { - return new Vector3f(pointOnA); - } - - @Nonnull - @Override - public Vector3f pointOnB() { - return new Vector3f(pointOnB); - } - - @Nonnull - @Override - public Vector3f normalOnB() { - return new Vector3f(normalOnB); - } -} diff --git a/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletNativeSpace.java b/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletNativeSpace.java deleted file mode 100644 index 6fa9ad71..00000000 --- a/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletNativeSpace.java +++ /dev/null @@ -1,164 +0,0 @@ -package dev.hytalemodding.impulse.bullet; - -import com.jme3.bullet.PhysicsSpace; -import com.jme3.bullet.StepFlag; -import com.jme3.bullet.collision.ManifoldPoints; -import com.jme3.bullet.collision.PersistentManifolds; -import com.jme3.bullet.collision.PhysicsCollisionObject; -import dev.hytalemodding.impulse.api.PhysicsContactPhase; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3f; - -final class BulletNativeSpace extends PhysicsSpace { - - private static final int CONTACT_EVENT_FLAGS = StepFlag.contactConceived - | StepFlag.contactStarted - | StepFlag.contactProcessed - | StepFlag.contactEnded; - private static final int MAX_CONTACT_EVENTS = 16_384; - private static final Vector3f ZERO_VECTOR = new Vector3f(); - - private final ArrayDeque contactEvents = new ArrayDeque<>(); - private final Map activeManifoldEvents = new HashMap<>(); - - BulletNativeSpace(@Nonnull BroadphaseType broadphaseType) { - super(broadphaseType); - } - - void updateWithContactEvents(float timeInterval, int maxSteps) { - contactEvents.clear(); - update(timeInterval, maxSteps, CONTACT_EVENT_FLAGS); - } - - @Nonnull - List drainContactEvents() { - if (contactEvents.isEmpty()) { - return List.of(); - } - List drained = new ArrayList<>(contactEvents); - contactEvents.clear(); - return drained; - } - - void forgetBody(long bodyId) { - Iterator> active = activeManifoldEvents.entrySet().iterator(); - while (active.hasNext()) { - BulletNativeContactEvent event = active.next().getValue(); - if (event.bodyAId() == bodyId || event.bodyBId() == bodyId) { - active.remove(); - } - } - contactEvents.removeIf(event -> event.bodyAId() == bodyId || event.bodyBId() == bodyId); - } - - @Override - public boolean onContactConceived(long pointId, - long manifoldId, - PhysicsCollisionObject pcoA, - PhysicsCollisionObject pcoB) { - boolean accepted = super.onContactConceived(pointId, manifoldId, pcoA, pcoB); - if (accepted) { - BulletNativeContactEvent event = eventFromPoint(PhysicsContactPhase.STARTED, - pcoA.nativeId(), - pcoB.nativeId(), - pointId); - activeManifoldEvents.put(manifoldId, event); - } - return accepted; - } - - @Override - public void onContactStarted(long manifoldId) { - super.onContactStarted(manifoldId); - BulletNativeContactEvent event = activeManifoldEvents.get(manifoldId); - if (event == null) { - event = eventFromManifold(PhysicsContactPhase.STARTED, manifoldId); - if (event != null) { - activeManifoldEvents.put(manifoldId, event); - } - } - if (event != null) { - addContactEvent(event.withPhase(PhysicsContactPhase.STARTED)); - } - } - - @Override - public void onContactProcessed(PhysicsCollisionObject pcoA, - PhysicsCollisionObject pcoB, - long pointId) { - super.onContactProcessed(pcoA, pcoB, pointId); - addContactEvent(eventFromPoint(PhysicsContactPhase.PERSISTED, - pcoA.nativeId(), - pcoB.nativeId(), - pointId)); - } - - @Override - public void onContactEnded(long manifoldId) { - super.onContactEnded(manifoldId); - BulletNativeContactEvent event = activeManifoldEvents.remove(manifoldId); - if (event != null) { - addContactEvent(event.withPhase(PhysicsContactPhase.ENDED)); - } - } - - private void addContactEvent(@Nonnull BulletNativeContactEvent event) { - if (contactEvents.size() < MAX_CONTACT_EVENTS) { - contactEvents.add(event); - } - } - - @Nullable - private static BulletNativeContactEvent eventFromManifold(@Nonnull PhysicsContactPhase phase, - long manifoldId) { - long bodyAId = PersistentManifolds.getBodyAId(manifoldId); - long bodyBId = PersistentManifolds.getBodyBId(manifoldId); - int pointCount = PersistentManifolds.countPoints(manifoldId); - if (pointCount <= 0) { - return new BulletNativeContactEvent(phase, - bodyAId, - bodyBId, - ZERO_VECTOR, - ZERO_VECTOR, - ZERO_VECTOR, - 0.0f, - 0.0f); - } - long pointId = PersistentManifolds.getPointId(manifoldId, 0); - return eventFromPoint(phase, bodyAId, bodyBId, pointId); - } - - @Nonnull - private static BulletNativeContactEvent eventFromPoint(@Nonnull PhysicsContactPhase phase, - long bodyAId, - long bodyBId, - long pointId) { - return new BulletNativeContactEvent(phase, - bodyAId, - bodyBId, - fromJmeVector(ManifoldPoints::getPositionWorldOnA, pointId), - fromJmeVector(ManifoldPoints::getPositionWorldOnB, pointId), - fromJmeVector(ManifoldPoints::getNormalWorldOnB, pointId), - ManifoldPoints.getDistance1(pointId), - ManifoldPoints.getAppliedImpulse(pointId)); - } - - @Nonnull - private static Vector3f fromJmeVector(@Nonnull ManifoldVectorReader reader, long pointId) { - com.jme3.math.Vector3f out = new com.jme3.math.Vector3f(); - reader.read(pointId, out); - return new Vector3f(out.x, out.y, out.z); - } - - @FunctionalInterface - private interface ManifoldVectorReader { - void read(long pointId, com.jme3.math.Vector3f out); - } -} diff --git a/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletSpace.java b/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletSpace.java deleted file mode 100644 index 313a54d5..00000000 --- a/impulse-bullet/src/main/java/dev/hytalemodding/impulse/bullet/BulletSpace.java +++ /dev/null @@ -1,643 +0,0 @@ -package dev.hytalemodding.impulse.bullet; - -import com.jme3.bullet.collision.ManifoldPoints; -import com.jme3.bullet.collision.PersistentManifolds; -import com.jme3.bullet.collision.PhysicsCollisionObject; -import com.jme3.bullet.collision.PhysicsRayTestResult; -import com.jme3.bullet.collision.shapes.BoxCollisionShape; -import com.jme3.bullet.collision.shapes.CapsuleCollisionShape; -import com.jme3.bullet.collision.shapes.CollisionShape; -import com.jme3.bullet.collision.shapes.ConeCollisionShape; -import com.jme3.bullet.collision.shapes.CylinderCollisionShape; -import com.jme3.bullet.collision.shapes.PlaneCollisionShape; -import com.jme3.bullet.collision.shapes.SphereCollisionShape; -import com.jme3.bullet.joints.HingeJoint; -import com.jme3.bullet.joints.Point2PointJoint; -import com.jme3.bullet.joints.SixDofJoint; -import com.jme3.bullet.joints.SixDofSpringJoint; -import com.jme3.bullet.joints.SliderJoint; -import com.jme3.bullet.objects.PhysicsRigidBody; -import com.jme3.math.Matrix3f; -import com.jme3.math.Plane; -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBackendEventKind; -import dev.hytalemodding.impulse.api.PhysicsBackendEventSink; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsContact; -import dev.hytalemodding.impulse.api.PhysicsJoint; -import dev.hytalemodding.impulse.api.PhysicsJointType; -import dev.hytalemodding.impulse.api.PhysicsRayHit; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.capability.PhysicsBackendEventsCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsCapabilityDescriptor; -import dev.hytalemodding.impulse.api.capability.PhysicsContinuousCollisionCapability; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -public final class BulletSpace implements PhysicsSpace { - - private static final PhysicsContinuousCollisionCapability CONTINUOUS_COLLISION_CAPABILITY = - new PhysicsContinuousCollisionCapability() { - }; - private static final PhysicsBackendEventsCapability BACKEND_EVENTS_CAPABILITY = - () -> Set.of(PhysicsBackendEventKind.CONTACT_STARTED, - PhysicsBackendEventKind.CONTACT_PERSISTED, - PhysicsBackendEventKind.CONTACT_ENDED); - private static final List CAPABILITY_DESCRIPTORS = List.of( - PhysicsContinuousCollisionCapability.DESCRIPTOR, - PhysicsBackendEventsCapability.DESCRIPTOR); - - private final SpaceId id; - private final BulletBackend backend; - private final BulletNativeSpace space; - private final Map bodiesByRigidBody = new IdentityHashMap<>(); - private final Map bodiesByNativeId = new HashMap<>(); - private final List bodies = new ArrayList<>(); - private final List joints = new ArrayList<>(); - private boolean closed; - - BulletSpace(@Nonnull SpaceId id, - @Nonnull BulletBackend backend, - @Nonnull BulletNativeSpace space) { - this.id = id; - this.backend = backend; - this.space = space; - } - - @Nonnull - @Override - public SpaceId id() { - return id; - } - - @Nonnull - @Override - public BackendId backendId() { - return backend.getId(); - } - - @Override - public void step(float dt) { - requireOpen(); - space.update(dt, 0); - } - - @Override - public void step(float dt, @Nonnull PhysicsBackendEventSink events) { - Objects.requireNonNull(events, "events"); - requireOpen(); - space.updateWithContactEvents(dt, 0); - for (BulletNativeContactEvent event : space.drainContactEvents()) { - BulletBody bodyA = bodiesByNativeId.get(event.bodyAId()); - BulletBody bodyB = bodiesByNativeId.get(event.bodyBId()); - if (bodyA == null || bodyB == null) { - continue; - } - events.contact(event.phase(), - bodyA, - bodyB, - event.pointOnA(), - event.pointOnB(), - event.normalOnB(), - event.distance(), - event.impulse()); - } - } - - @Override - public void setGravity(float x, float y, float z) { - requireOpen(); - space.setGravity(toJme(x, y, z)); - } - - @Nonnull - @Override - public Vector3f getGravity() { - requireOpen(); - return fromJme(space.getGravity(new com.jme3.math.Vector3f())); - } - - @Override - public void addBody(@Nonnull PhysicsBody body) { - requireOpen(); - if (!(body instanceof BulletBody bulletBody)) { - throw new IllegalArgumentException("Body does not belong to bullet backend"); - } - requireBodyAddable(bulletBody); - space.addCollisionObject(bulletBody.getRigidBody()); - trackBody(bulletBody); - bulletBody.markAttachedTo(this); - } - - @Override - public void removeBody(@Nonnull PhysicsBody body) { - requireOpen(); - if (!(body instanceof BulletBody bulletBody)) { - return; - } - if (!ownsBody(bulletBody)) { - return; - } - - removeAttachedJoints(bulletBody); - if (bulletBody.isAttachedToSpace()) { - space.removeCollisionObject(bulletBody.getRigidBody()); - } - space.forgetBody(bulletBody.getNativeId()); - untrackBody(bulletBody); - } - - @Nonnull - @Override - public List getBodies() { - requireOpen(); - for (PhysicsRigidBody rigidBody : space.getRigidBodyList()) { - wrapBody(rigidBody); - } - List attachedBodies = new ArrayList<>(space.getRigidBodyList().size()); - for (BulletBody body : bodies) { - if (body.isAttachedToSpace()) { - attachedBodies.add(body); - } - } - return attachedBodies; - } - - @Override - public int bodyCount() { - requireOpen(); - return space.getRigidBodyList().size(); - } - - @Override - public void forEachBody(@Nonnull Consumer consumer) { - requireOpen(); - for (PhysicsRigidBody rigidBody : space.getRigidBodyList()) { - consumer.accept(wrapBody(rigidBody)); - } - } - - @Override - public boolean containsBody(@Nonnull PhysicsBody body) { - requireOpen(); - return body instanceof BulletBody bulletBody && ownsBody(bulletBody) - && bulletBody.isAttachedToSpace(); - } - - @Override - public void snapshotBodies(@Nonnull Consumer consumer) { - snapshotBodies(_ -> null, consumer); - } - - @Override - public void snapshotBodies(@Nonnull Function previousSnapshots, - @Nonnull Consumer consumer) { - snapshotBodies(previousSnapshots, (_, snapshot) -> consumer.accept(snapshot)); - } - - @Override - public void snapshotBodies(@Nonnull Function previousSnapshots, - @Nonnull BiConsumer consumer) { - requireOpen(); - for (BulletBody body : bodies) { - if (body.isAttachedToSpace()) { - consumer.accept(body, PhysicsBodySnapshot.from(body, previousSnapshots.apply(body))); - } - } - } - - @Nonnull - @Override - public Optional getCapability(@Nonnull Class type) { - Objects.requireNonNull(type, "type"); - if (type == PhysicsContinuousCollisionCapability.class) { - return Optional.of(type.cast(CONTINUOUS_COLLISION_CAPABILITY)); - } - if (type == PhysicsBackendEventsCapability.class) { - return Optional.of(type.cast(BACKEND_EVENTS_CAPABILITY)); - } - return Optional.empty(); - } - - @Nonnull - @Override - public List getCapabilityDescriptors() { - return CAPABILITY_DESCRIPTORS; - } - - @Nonnull - @Override - public PhysicsBody createStaticPlane(float groundY) { - requireOpen(); - Plane plane = new Plane(com.jme3.math.Vector3f.UNIT_Y, 0.0f); - CollisionShape shape = new PlaneCollisionShape(plane); - PhysicsRigidBody body = new PhysicsRigidBody(shape, - com.jme3.bullet.objects.PhysicsBody.massForStatic); - body.setPhysicsLocation(toJme(0.0f, groundY, 0.0f)); - return trackBody(new BulletBody(body)); - } - - @Nonnull - @Override - public PhysicsBody createBox(float halfX, float halfY, float halfZ, float mass) { - requireOpen(); - CollisionShape shape = new BoxCollisionShape(toJme(halfX, halfY, halfZ)); - return trackBody(new BulletBody(new PhysicsRigidBody(shape, mass))); - } - - @Nonnull - @Override - public PhysicsBody createBox(@Nonnull Vector3f halfExtents, float mass) { - return createBox(halfExtents.x, halfExtents.y, halfExtents.z, mass); - } - - @Nonnull - @Override - public PhysicsBody createSphere(float radius, float mass) { - requireOpen(); - return trackBody(new BulletBody(new PhysicsRigidBody(new SphereCollisionShape(radius), mass))); - } - - @Nonnull - @Override - public PhysicsBody createCapsule(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - requireOpen(); - CollisionShape shape = new CapsuleCollisionShape(radius, halfHeight * 2f, axis.index()); - return trackBody(new BulletBody(new PhysicsRigidBody(shape, mass))); - } - - @Nonnull - @Override - public PhysicsBody createCylinder(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - requireOpen(); - CollisionShape shape = new CylinderCollisionShape(radius, halfHeight * 2f, axis.index()); - return trackBody(new BulletBody(new PhysicsRigidBody(shape, mass))); - } - - @Nonnull - @Override - public PhysicsBody createCone(float radius, float halfHeight, @Nonnull PhysicsAxis axis, float mass) { - requireOpen(); - CollisionShape shape = new ConeCollisionShape(radius, halfHeight * 2f, axis.index()); - return trackBody(new BulletBody(new PhysicsRigidBody(shape, mass))); - } - - @Nonnull - @Override - public Optional raycastClosest(@Nonnull Vector3f from, @Nonnull Vector3f to) { - requireOpen(); - List hits = raycastAll(from, to); - PhysicsRayHit closest = null; - for (PhysicsRayHit hit : hits) { - if (closest == null || hit.fraction() < closest.fraction()) { - closest = hit; - } - } - return Optional.ofNullable(closest); - } - - @Nonnull - @Override - public List raycastAll(@Nonnull Vector3f from, @Nonnull Vector3f to) { - requireOpen(); - List results = space.rayTest(toJme(from), toJme(to)); - List hits = new ArrayList<>(results.size()); - Vector3f delta = new Vector3f(to).sub(from); - float distance = delta.length(); - for (PhysicsRayTestResult result : results) { - PhysicsCollisionObject collisionObject = result.getCollisionObject(); - if (!(collisionObject instanceof PhysicsRigidBody rigidBody)) { - continue; - } - BulletBody body = wrapBody(rigidBody); - float fraction = result.getHitFraction(); - Vector3f point = new Vector3f(from).fma(fraction, delta); - Vector3f normal = fromJme(result.getHitNormalLocal(new com.jme3.math.Vector3f())); - hits.add(new PhysicsRayHit(body, point, normal, fraction, distance * fraction)); - } - return hits; - } - - @Nonnull - @Override - public List getContacts() { - requireOpen(); - long[] manifolds = space.listManifoldIds(); - List contacts = new ArrayList<>(); - for (long manifold : manifolds) { - BulletBody bodyA = bodiesByNativeId.get(PersistentManifolds.getBodyAId(manifold)); - BulletBody bodyB = bodiesByNativeId.get(PersistentManifolds.getBodyBId(manifold)); - if (bodyA == null || bodyB == null) { - continue; - } - int pointCount = PersistentManifolds.countPoints(manifold); - for (int i = 0; i < pointCount; i++) { - long pointId = PersistentManifolds.getPointId(manifold, i); - Vector3f pointOnA = fromJmeVector(ManifoldPoints::getPositionWorldOnA, pointId); - Vector3f pointOnB = fromJmeVector(ManifoldPoints::getPositionWorldOnB, pointId); - Vector3f normal = fromJmeVector(ManifoldPoints::getNormalWorldOnB, pointId); - contacts.add(new PhysicsContact(bodyA, bodyB, pointOnA, pointOnB, normal, - ManifoldPoints.getDistance1(pointId), - ManifoldPoints.getAppliedImpulse(pointId))); - } - } - return contacts; - } - - @Nonnull - @Override - public PhysicsJoint createFixedJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB) { - requireOpen(); - BulletBody bulletA = requireBody(bodyA); - BulletBody bulletB = requireBody(bodyB); - SixDofJoint joint = new SixDofJoint(bulletA.getRigidBody(), bulletB.getRigidBody(), - toJme(anchorA), toJme(anchorB), true); - joint.setLinearLowerLimit(toJme(0f, 0f, 0f)); - joint.setLinearUpperLimit(toJme(0f, 0f, 0f)); - joint.setAngularLowerLimit(toJme(0f, 0f, 0f)); - joint.setAngularUpperLimit(toJme(0f, 0f, 0f)); - return addJoint(new BulletJoint(PhysicsJointType.FIXED, bulletA, bulletB, joint, - anchorA, anchorB, null)); - } - - @Nonnull - @Override - public PhysicsJoint createPointJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB) { - requireOpen(); - BulletBody bulletA = requireBody(bodyA); - BulletBody bulletB = requireBody(bodyB); - Point2PointJoint joint = new Point2PointJoint(bulletA.getRigidBody(), bulletB.getRigidBody(), - toJme(anchorA), toJme(anchorB)); - return addJoint(new BulletJoint(PhysicsJointType.POINT, bulletA, bulletB, joint, - anchorA, anchorB, null)); - } - - @Nonnull - @Override - public PhysicsJoint createHingeJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis) { - requireOpen(); - BulletBody bulletA = requireBody(bodyA); - BulletBody bulletB = requireBody(bodyB); - Vector3f normalizedAxis = normalizedOrDefault(axis); - HingeJoint joint = new HingeJoint(bulletA.getRigidBody(), bulletB.getRigidBody(), - toJme(anchorA), toJme(anchorB), toJme(normalizedAxis), toJme(normalizedAxis)); - return addJoint(new BulletJoint(PhysicsJointType.HINGE, bulletA, bulletB, joint, - anchorA, anchorB, normalizedAxis)); - } - - @Nonnull - @Override - public PhysicsJoint createSliderJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis) { - requireOpen(); - BulletBody bulletA = requireBody(bodyA); - BulletBody bulletB = requireBody(bodyB); - Vector3f normalizedAxis = normalizedOrDefault(axis); - Matrix3f basis = basisForAxis(normalizedAxis); - SliderJoint joint = new SliderJoint(bulletA.getRigidBody(), bulletB.getRigidBody(), - toJme(anchorA), toJme(anchorB), basis, basis, true); - return addJoint(new BulletJoint(PhysicsJointType.SLIDER, bulletA, bulletB, joint, - anchorA, anchorB, normalizedAxis)); - } - - @Nonnull - @Override - public PhysicsJoint createSpringJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - float restLength, - float stiffness, - float damping) { - requireOpen(); - BulletBody bulletA = requireBody(bodyA); - BulletBody bulletB = requireBody(bodyB); - SixDofSpringJoint joint = new SixDofSpringJoint(bulletA.getRigidBody(), bulletB.getRigidBody(), - toJme(anchorA), toJme(anchorB), Matrix3f.IDENTITY, - Matrix3f.IDENTITY, true); - joint.enableSpring(0, true); - joint.setStiffness(0, stiffness); - joint.setDamping(0, damping); - joint.setEquilibriumPoint(0); - BulletJoint wrapped = new BulletJoint(PhysicsJointType.SPRING, bulletA, bulletB, joint, - anchorA, anchorB, new Vector3f(1f, 0f, 0f), restLength, stiffness, damping); - wrapped.setLimits(-restLength, restLength); - return addJoint(wrapped); - } - - @Override - public void removeJoint(@Nonnull PhysicsJoint joint) { - requireOpen(); - if (!(joint instanceof BulletJoint bulletJoint)) { - return; - } - removeJointInternal(bulletJoint); - } - - @Nonnull - @Override - public List getJoints() { - requireOpen(); - return new ArrayList<>(joints); - } - - @Override - public int jointCount() { - requireOpen(); - return joints.size(); - } - - @Override - public void forEachJoint(@Nonnull Consumer consumer) { - requireOpen(); - for (BulletJoint joint : joints) { - consumer.accept(joint); - } - } - - @Override - public void close() { - if (closed) { - return; - } - - closed = true; - try { - for (BulletJoint joint : new ArrayList<>(joints)) { - removeJointInternal(joint); - } - - for (BulletBody body : new ArrayList<>(bodies)) { - if (body.isAttachedToSpace()) { - space.removeCollisionObject(body.getRigidBody()); - } - space.forgetBody(body.getNativeId()); - body.invalidateFrom(this); - } - bodies.clear(); - bodiesByRigidBody.clear(); - bodiesByNativeId.clear(); - } finally { - space.destroy(); - } - } - - private BulletJoint addJoint(@Nonnull BulletJoint joint) { - space.addJoint(joint.getNativeJoint()); - joints.add(joint); - return joint; - } - - private BulletBody requireBody(@Nonnull PhysicsBody body) { - if (!(body instanceof BulletBody bulletBody)) { - throw new IllegalArgumentException("Body does not belong to bullet backend"); - } - if (!ownsBody(bulletBody)) { - throw new IllegalArgumentException("Body does not belong to this bullet space"); - } - bulletBody.requireNotInvalidated(); - return bulletBody; - } - - private BulletBody trackBody(@Nonnull BulletBody body) { - body.bindTo(this); - if (!bodiesByRigidBody.containsKey(body.getRigidBody())) { - bodiesByRigidBody.put(body.getRigidBody(), body); - bodiesByNativeId.put(body.getNativeId(), body); - bodies.add(body); - } - return body; - } - - private void untrackBody(@Nonnull BulletBody body) { - bodiesByRigidBody.remove(body.getRigidBody()); - bodiesByNativeId.remove(body.getNativeId()); - bodies.remove(body); - body.detachFrom(this); - } - - private void removeAttachedJoints(@Nonnull BulletBody body) { - for (BulletJoint joint : new ArrayList<>(joints)) { - if (joint.getBodyA() != body && joint.getBodyB() != body) { - continue; - } - - removeJointInternal(joint); - } - } - - private BulletBody wrapBody(@Nonnull PhysicsRigidBody rigidBody) { - BulletBody existing = bodiesByRigidBody.get(rigidBody); - if (existing != null) { - return existing; - } - BulletBody body = trackBody(new BulletBody(rigidBody)); - body.markAttachedTo(this); - return body; - } - - private void removeJointInternal(@Nonnull BulletJoint joint) { - if (!joints.remove(joint)) { - return; - } - space.removeJoint(joint.getNativeJoint()); - } - - private void requireBodyAddable(@Nonnull BulletBody body) { - body.requireNotInvalidated(); - BulletSpace owner = body.getOwner(); - if (owner != null && owner != this) { - throw new IllegalArgumentException("Body belongs to another bullet space"); - } - if (body.isAttachedToSpace()) { - throw new IllegalStateException("Body is already attached to a bullet space"); - } - } - - private boolean ownsBody(@Nonnull BulletBody body) { - return body.isOwnedBy(this) && bodiesByRigidBody.get(body.getRigidBody()) == body; - } - - private void requireOpen() { - if (closed) { - throw new IllegalStateException("Bullet space is closed: " + id); - } - } - - private static Vector3f normalizedOrDefault(@Nonnull Vector3f axis) { - Vector3f normalized = new Vector3f(axis); - if (normalized.lengthSquared() == 0f) { - normalized.set(0f, 1f, 0f); - } - return normalized.normalize(); - } - - private static Matrix3f basisForAxis(@Nonnull Vector3f axis) { - Vector3f x = normalizedOrDefault(axis); - Vector3f up = Math.abs(x.y) < 0.9f - ? new Vector3f(0f, 1f, 0f) - : new Vector3f(1f, 0f, 0f); - Vector3f z = new Vector3f(x).cross(up).normalize(); - Vector3f y = new Vector3f(z).cross(x).normalize(); - Matrix3f basis = new Matrix3f(); - basis.fromAxes(toJme(x), toJme(y), toJme(z)); - return basis; - } - - private static com.jme3.math.Vector3f toJme(@Nonnull Vector3f vector) { - return toJme(vector.x, vector.y, vector.z); - } - - private static com.jme3.math.Vector3f toJme(float x, float y, float z) { - return new com.jme3.math.Vector3f(x, y, z); - } - - private static Vector3f fromJme(@Nonnull com.jme3.math.Vector3f vector) { - return new Vector3f(vector.x, vector.y, vector.z); - } - - private static Vector3f fromJmeVector(@Nonnull ManifoldVectorReader reader, long pointId) { - com.jme3.math.Vector3f out = new com.jme3.math.Vector3f(); - reader.read(pointId, out); - return fromJme(out); - } - - @FunctionalInterface - private interface ManifoldVectorReader { - void read(long pointId, com.jme3.math.Vector3f out); - } -} diff --git a/impulse-bullet/src/main/resources/META-INF/services/dev.hytalemodding.impulse.api.PhysicsBackend b/impulse-bullet/src/main/resources/META-INF/services/dev.hytalemodding.impulse.api.PhysicsBackend deleted file mode 100644 index 71f3c60f..00000000 --- a/impulse-bullet/src/main/resources/META-INF/services/dev.hytalemodding.impulse.api.PhysicsBackend +++ /dev/null @@ -1 +0,0 @@ -dev.hytalemodding.impulse.bullet.BulletBackend \ No newline at end of file diff --git a/impulse-core/README.md b/impulse-core/README.md index 45318060..4c3c1d5e 100644 --- a/impulse-core/README.md +++ b/impulse-core/README.md @@ -13,16 +13,16 @@ Impulse core is divided in two categories: - `/impulse backend list` - list discovered backends and active physics spaces. -Backend jars are Java service-provider jars. Impulse discovers `PhysicsBackend` providers from jars -anywhere under the configured Hytale `mods` directories. +Backend jars are Java service-provider jars. Impulse discovers `PhysicsBackendRuntimeProvider` +services from jars anywhere under the configured Hytale `mods` directories. ## Event frames -`PhysicsWorldResource.getLatestEventFrame()` exposes the latest value-only physics event frame for -diagnostics. When collection is enabled, backends emit bounded post-step `PhysicsBackendEvent` -batches; core translates them to stable `PhysicsFrameEvent` values keyed by `RigidBodyKey` and -`JointKey`, then publishes one `PhysicsEventFramePublishedEvent` Hytale world event for the -completed frame. +`PhysicsWorlds.latestEventFrame(physicsStore)` exposes the latest value-only physics event frame +for diagnostics. When collection is enabled, backends emit bounded post-step event +batches; core translates them to stable UUID-primary `PhysicsFrameEvent` values and copied +PhysicsStore refs where available, then publishes one `PhysicsEventFramePublishedEvent` Hytale +world event for the completed frame. Backend event collection is opt-in through `PhysicsWorldSettings.setEventCollectionMode(...)`. Worlds default to `PhysicsEventCollectionMode.DISABLED`; use @@ -37,43 +37,17 @@ world and `/impulse settings simulation events disabled` to return to the defaul ## Profiling Spark plugin is advised to profile threaded physics benchmarks. By using the following command, -the exported profile includes both Hytale world threads and Impulse's physics owner-lane executor threads: +the exported profile includes Hytale world/store tick threads and PhysicsStore completion work: ```bash -/spark profiler start --timeout 60 --save-to-file --regex --not-combined --ignore-sleeping --thread WorldThread.* --thread Impulse.*physics.*owner.* --thread ChunkLighting.* --thread WorldMap.* +/spark profiler start --timeout 60 --save-to-file --regex --not-combined --ignore-sleeping --thread WorldThread.* --thread Impulse.*PhysicsStore.* --thread ChunkLighting.* --thread WorldMap.* ``` Avoid contact debug rendering during benchmark captures; it calls backend contact enumeration and will distort the hot path. -## Crucible tests +## Runtime validation -Install the patched Crucible runtime jar: - -```bash -./scripts/ci/install-crucible-runtime.sh -``` - -Run the smoke-tagged runtime suite: - -```bash -JAVA_TOOL_OPTIONS="-Dcrucible.autorun=true -Dcrucible.tags=smoke" \ - ./gradlew runAllMods -``` - -Run the live-tagged runtime suite: - -```bash -JAVA_TOOL_OPTIONS="-Dcrucible.autorun=true -Dcrucible.tags=live" \ - ./gradlew runAllMods -``` - -Run the detached full-collision streaming benchmark scenario: - -```bash -JAVA_TOOL_OPTIONS="-Dcrucible.autorun=true -Dcrucible.tags=benchmark" \ - ./gradlew runAllMods -``` - -Crucible selects `impulse:rapier` when it is installed. Override that only for backend-specific -debugging with `-Dimpulse.crucible.backend=`. +Use ordinary Gradle tests for backend physics, PhysicsStore topology, settings round trips, and +module lifecycle predicates. When a failure depends on live Hytale server behavior, reproduce it +with `./gradlew runAllMods` and document the world setup and commands used. diff --git a/impulse-core/build.gradle.kts b/impulse-core/build.gradle.kts index 661595dd..4b9109c7 100644 --- a/impulse-core/build.gradle.kts +++ b/impulse-core/build.gradle.kts @@ -4,11 +4,7 @@ plugins { version = rootProject.version -repositories { - maven { - url = uri("https://gitlab.com/api/v4/projects/82033924/packages/maven") - } -} +evaluationDependsOn(":impulse-backends:api") val coreModuleName = "dev.hytalemodding.impulse.core" // These parent dependencies are shared by the core plugin and inherited by bundled subplugins. @@ -25,24 +21,28 @@ val moduleInfoModulePath by configurations.creating { } dependencies { - implementation(project(":impulse-api")) - testImplementation(testFixtures(project(":impulse-api"))) - testImplementation(libs.objenesis) - testCompileOnly("com.hypixel.hytale:Server:${property("hytale_version") as String}") - testRuntimeOnly("com.hypixel.hytale:Server:${property("hytale_version") as String}") - compileOnly(libs.crucible) + implementation(project(":impulse-backends:api")) + compileOnly(project(":impulse-early-plugin")) + compileOnly(libs.lombok) moduleInfoModulePath(libs.joml) moduleInfoModulePath(libs.jsr305) - moduleInfoModulePath(libs.crucible) - compileOnly(libs.lombok) annotationProcessor(libs.lombok) + + testImplementation(testFixtures(project(":impulse-backends:api"))) + testImplementation(libs.objenesis) + testCompileOnly(project(":impulse-early-plugin")) + testRuntimeOnly(project(":impulse-early-plugin")) + testCompileOnly("com.hypixel.hytale:Server:${property("hytale_version") as String}") + testRuntimeOnly("com.hypixel.hytale:Server:${property("hytale_version") as String}") + } -val impulseApiJar = project(":impulse-api").tasks.named("jar") +val impulseApiJar = project(":impulse-backends:api").tasks.named("jar") +val controlBuiltinProject = project(":impulse-builtins:control") -tasks.named("compileJava") { +tasks.named("compileJava") { doFirst { destinationDirectory.file("module-info.class").get().asFile.delete() } @@ -80,9 +80,14 @@ tasks.named("classes") { tasks.named("jar") { dependsOn(compileCoreModuleInfo) + dependsOn(":impulse-builtins:control:classes") from(compileCoreModuleInfo.flatMap { it.destinationDirectory }) { include("module-info.class") } + from(controlBuiltinProject.layout.buildDirectory.dir("classes/java/main")) + from(controlBuiltinProject.layout.buildDirectory.dir("resources/main")) { + exclude("manifest.json") + } } tasks.withType().configureEach { @@ -97,18 +102,59 @@ hytaleTools { modDescription = property("mod_description") as String manifestServerVersion = property("hytale_version") as String manifestDependencies = impulseManifestDependencies - manifestOptionalDependencies = "com.ionforgelabs:crucible=*" subPlugin ( - "ImpulseWorldCollision", - "dev.hytalemodding.impulse.core.plugin.modules.worldcollision.ImpulseWorldCollisionPlugin", + "ImpulsePhysicsEntity", + "dev.hytalemodding.impulse.core.internal.modules.physicsentity.PhysicsEntityModule", false, /* disabledByDefault */ false /* includeAssetPack */ ) + subPlugin ( "ImpulseControl", - "dev.hytalemodding.impulse.core.plugin.modules.control.ImpulseControlPlugin", + "dev.hytalemodding.impulse.builtin.control.ImpulseControlPlugin", false, /* disabledByDefault */ false /* includeAssetPack */ ) + + subPlugin ( + "ImpulsePhysicsChunk", + "dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkSubPlugin", + false, /* disabledByDefault */ + false /* includeAssetPack */ + ) +} + +tasks.named("updatePluginManifest") { + doLast { + val manifestFile = file("src/main/resources/manifest.json") + + @Suppress("UNCHECKED_CAST") + val manifestJson = groovy.json.JsonSlurper().parse(manifestFile) + as MutableMap + + @Suppress("UNCHECKED_CAST") + val subPlugins = manifestJson["SubPlugins"] as? List> + ?: return@doLast + + fun MutableMap.mergeLoadBefore(loadBefore: Map) { + @Suppress("UNCHECKED_CAST") + val existingLoadBefore = (this["LoadBefore"] as? Map) + ?.toMutableMap() + ?: linkedMapOf() + existingLoadBefore.putAll(loadBefore) + this["LoadBefore"] = existingLoadBefore + } + + subPlugins.firstOrNull { it["Name"] == "ImpulsePhysicsEntity" } + ?.mergeLoadBefore(mapOf( + "HytaleModding:ImpulseControl" to "*", + "HytaleModding:ImpulsePhysicsChunk" to "*" + )) + + manifestFile.writeText( + groovy.json.JsonOutput.prettyPrint(groovy.json.JsonOutput.toJson(manifestJson)) + + System.lineSeparator() + ) + } } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/BackendDiscovery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/BackendDiscovery.java index 4d866729..5d6e6430 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/BackendDiscovery.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/BackendDiscovery.java @@ -2,7 +2,6 @@ import com.hypixel.hytale.logger.HytaleLogger; import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.PhysicsBackend; import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; import java.io.IOException; import java.net.MalformedURLException; @@ -23,41 +22,15 @@ import java.util.stream.Stream; import javax.annotation.Nonnull; -@SuppressWarnings("removal") final class BackendDiscovery { private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); - private static final String BACKEND_SERVICE_RESOURCE = - "META-INF/services/" + PhysicsBackend.class.getName(); private static final String RUNTIME_PROVIDER_SERVICE_RESOURCE = "META-INF/services/" + PhysicsBackendRuntimeProvider.class.getName(); private BackendDiscovery() { } - @Nonnull - static List discover(@Nonnull Collection backendSearchRoots, - @Nonnull ClassLoader parentClassLoader) { - Map discovered = new LinkedHashMap<>(); - loadFrom(parentClassLoader, "plugin classpath", discovered); - - for (Path backendJar : findBackendProviderJars(backendSearchRoots)) { - try { - URL[] urls = {backendJar.toUri().toURL()}; - URLClassLoader backendLoader = new URLClassLoader( - "ImpulseBackendProvider(" + backendJar.getFileName() + ")", - urls, - parentClassLoader); - loadFrom(backendLoader, backendJar.toString(), discovered); - } catch (MalformedURLException e) { - LOGGER.at(Level.WARNING) - .log("Skipping backend provider jar %s: %s", backendJar, e.getMessage()); - } - } - - return List.copyOf(discovered.values()); - } - @Nonnull static List discoverRuntimeProviders( @Nonnull Collection backendSearchRoots, @@ -95,7 +68,7 @@ private static List findBackendProviderJars( paths.filter(Files::isRegularFile) .filter(BackendDiscovery::isJar) .sorted(Comparator.comparing(Path::toString)) - .filter(BackendDiscovery::containsBackendService) + .filter(BackendDiscovery::containsRuntimeProviderService) .forEach(jars::add); } catch (IOException e) { LOGGER.at(Level.WARNING) @@ -111,10 +84,9 @@ private static boolean isJar(@Nonnull Path path) { return path.getFileName().toString().toLowerCase().endsWith(".jar"); } - private static boolean containsBackendService(@Nonnull Path jarPath) { + private static boolean containsRuntimeProviderService(@Nonnull Path jarPath) { try (JarFile jar = new JarFile(jarPath.toFile())) { - return jar.getEntry(BACKEND_SERVICE_RESOURCE) != null - || jar.getEntry(RUNTIME_PROVIDER_SERVICE_RESOURCE) != null; + return jar.getEntry(RUNTIME_PROVIDER_SERVICE_RESOURCE) != null; } catch (IOException e) { LOGGER.at(Level.WARNING) .log("Skipping unreadable backend provider jar %s: %s", @@ -124,23 +96,6 @@ private static boolean containsBackendService(@Nonnull Path jarPath) { } } - private static void loadFrom(@Nonnull ClassLoader classLoader, - @Nonnull String source, - @Nonnull Map discovered) { - ServiceLoader loader = ServiceLoader.load(PhysicsBackend.class, - classLoader); - try { - for (PhysicsBackend backend : loader) { - discovered.put(backend.getId(), backend); - } - } catch (ServiceConfigurationError e) { - LOGGER.at(Level.WARNING) - .log("Failed to load physics backend provider from %s: %s", - source, - e.getMessage()); - } - } - private static void loadRuntimeProvidersFrom(@Nonnull ClassLoader classLoader, @Nonnull String source, @Nonnull Map discovered) { diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/ImpulsePlugin.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/ImpulsePlugin.java index 20a067fd..f3b0f2c3 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/ImpulsePlugin.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/ImpulsePlugin.java @@ -1,60 +1,21 @@ package dev.hytalemodding.impulse.core; import com.hypixel.hytale.component.ComponentRegistryProxy; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.ResourceType; -import com.hypixel.hytale.component.SystemGroup; -import com.hypixel.hytale.component.event.WorldEventType; -import com.hypixel.hytale.common.plugin.PluginIdentifier; import com.hypixel.hytale.logger.HytaleLogger; -import com.hypixel.hytale.server.core.HytaleServer; import com.hypixel.hytale.server.core.Options; import com.hypixel.hytale.server.core.command.system.CommandRegistry; import com.hypixel.hytale.server.core.plugin.JavaPlugin; import com.hypixel.hytale.server.core.plugin.JavaPluginInit; -import com.hypixel.hytale.server.core.plugin.PluginBase; import com.hypixel.hytale.server.core.plugin.PluginManager; -import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsBackend; +import dev.hytalemodding.impulse.api.ImpulseBackendRegistry; import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; -import dev.hytalemodding.impulse.core.internal.commands.ImpulseCommandContributionRegistry; -import dev.hytalemodding.impulse.core.internal.components.GeneratedVisualProxyComponent; +import dev.hytalemodding.impulse.core.internal.commands.ImpulseCommandTreeRegistry; import dev.hytalemodding.impulse.core.internal.modules.ImpulseSubPluginRegistration; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsDebugResource; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerLaneScheduler; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerResource; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import dev.hytalemodding.impulse.core.internal.systems.debug.PhysicsDebugSystem; -import dev.hytalemodding.impulse.core.internal.systems.body.PhysicsBodyIdentityCleanupSystem; -import dev.hytalemodding.impulse.core.internal.systems.body.RigidBodyLifecycleCleanupSystem; -import dev.hytalemodding.impulse.core.internal.systems.body.RigidBodyReconciliationSystem; -import dev.hytalemodding.impulse.core.internal.systems.persistence.PersistentPhysicsBodyHydrationSystem; -import dev.hytalemodding.impulse.core.internal.systems.persistence.PersistentPhysicsJointHydrationSystem; -import dev.hytalemodding.impulse.core.internal.systems.persistence.PersistentPhysicsSpaceBootstrapSystem; -import dev.hytalemodding.impulse.core.internal.systems.persistence.PersistentPhysicsWorldSyncSystem; -import dev.hytalemodding.impulse.core.internal.systems.persistence.PhysicsRuntimeHolderSystem; -import dev.hytalemodding.impulse.core.internal.systems.publication.PhysicsSnapshotPublicationSystem; -import dev.hytalemodding.impulse.core.internal.systems.step.PhysicsStepSystem; -import dev.hytalemodding.impulse.core.internal.systems.sync.PhysicsBodyAttachmentIndexSystem; -import dev.hytalemodding.impulse.core.internal.systems.sync.PhysicsSyncSystem; -import dev.hytalemodding.impulse.core.internal.systems.visual.PhysicsDetachedVisualMaterializationSystem; -import dev.hytalemodding.impulse.core.internal.systems.owner.PhysicsOwnerLifecycleSystem; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyCollisionComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyDynamicsComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyIdentityComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyKinematicTargetComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyLifecycleComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyMaterialComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyShapeComponent; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFramePublishedEvent; -import dev.hytalemodding.impulse.core.plugin.persistence.PhysicsPersistenceResource; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.PhysicsStoreRegistration; +import dev.hytalemodding.impulse.core.internal.PhysicsStoreEarlyPluginProbe; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; @@ -62,69 +23,15 @@ import java.util.logging.Level; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import lombok.Getter; public final class ImpulsePlugin extends JavaPlugin { private static ImpulsePlugin instance; private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); - static final String OWNER_POOL_SIZE_PROPERTY = "impulse.ownerPool.size"; - - @Getter - private ComponentType physicsBodyAttachmentComponentType; - - @Getter - private ComponentType physicsBodyIdentityComponentType; - - @Getter - private ComponentType physicsBodyShapeComponentType; - - @Getter - private ComponentType physicsBodyDynamicsComponentType; - - @Getter - private ComponentType physicsBodyMaterialComponentType; - - @Getter - private ComponentType physicsBodyCollisionComponentType; - - @Getter - private ComponentType physicsBodyKinematicTargetComponentType; - - @Getter - private ComponentType physicsBodyLifecycleComponentType; - - @Getter - private ComponentType generatedVisualProxyComponentType; - - @Getter - private ResourceType physicsWorldResourceType; - - @Getter - private ResourceType physicsDebugResourceType; - - @Getter - private ResourceType physicsRuntimeProfilingResourceType; - - @Getter - private ResourceType physicsOwnerResourceType; - - @Getter - private ResourceType persistentPhysicsWorldResourceType; - - @Getter - private WorldEventType physicsEventFramePublishedEventType; - - @Getter - private SystemGroup persistenceRestoreGroup; @Nullable private BackendId defaultBackendId; - private PhysicsOwnerLaneScheduler physicsOwnerLaneScheduler; - private PhysicsStepSystem physicsStepSystem; - private PhysicsOwnerLifecycleSystem physicsOwnerLifecycleSystem; - public ImpulsePlugin(@Nonnull JavaPluginInit init) { super(init); instance = this; @@ -141,84 +48,39 @@ public BackendId getDefaultBackendId() { @Override protected void setup() { + PhysicsStoreEarlyPluginProbe.requireAvailable(); + + ComponentRegistryProxy physicsStoreRegistry = + PhysicsStoreRegistration.physicsStoreRegistry(this); + PhysicsComponentTypeRegistry.registerComponentTypes(physicsStoreRegistry); + PhysicsStoreRegistration.register(physicsStoreRegistry); ImpulseSubPluginRegistration.register(this); discoverBackends(); - registerComponents(); - registerSystems(); registerCommands(); } - @Override - protected void start() { - registerCrucibleSuites(); - } - @Override protected void shutdown() { - ImpulseCommandContributionRegistry.unregister(); - if (physicsStepSystem != null) { - physicsStepSystem.close(); - physicsStepSystem = null; - } - if (physicsOwnerLifecycleSystem != null) { - physicsOwnerLifecycleSystem.close(); - physicsOwnerLifecycleSystem = null; - } - if (physicsOwnerLaneScheduler != null) { - physicsOwnerLaneScheduler.close(); - physicsOwnerLaneScheduler = null; - } + ImpulseCommandTreeRegistry.unregister(); } - /** - * Registers optional Crucible suites after Crucible has loaded. - * Core owns these suites because they validate Impulse API and ECS behavior, - * not example command behavior. - */ - private void registerCrucibleSuites() { - try { - PluginManager pluginManager = HytaleServer.get().getPluginManager(); - PluginBase cruciblePlugin = pluginManager.getPlugin( - new PluginIdentifier("com.ionforgelabs", "crucible")); - if (cruciblePlugin == null) { - return; - } - ClassLoader crucibleLoader = ((JavaPlugin) cruciblePlugin).getClassLoader(); - Class suitesClass = Class.forName( - "dev.hytalemodding.impulse.core.internal.crucible.ImpulseCrucibleSuites", - true, - crucibleLoader); - suitesClass.getMethod("register", ClassLoader.class).invoke(null, crucibleLoader); - } catch (ClassNotFoundException e) { - // Crucible is not installed. - } catch (ReflectiveOperationException e) { - LOGGER.at(Level.WARNING) - .log("Failed to register Impulse Crucible suites: %s", e.getMessage()); - } - } - - @SuppressWarnings("removal") private void discoverBackends() { for (PhysicsBackendRuntimeProvider provider : BackendDiscovery.discoverRuntimeProviders( backendSearchRoots(), getClassLoader())) { - Impulse.registerRuntimeProvider(provider); - } - for (PhysicsBackend backend : BackendDiscovery.discover(backendSearchRoots(), - getClassLoader())) { - Impulse.registerBackend(backend); + ImpulseBackendRegistry.registerRuntimeProvider(provider); } - for (PhysicsBackendRuntimeProvider provider : Impulse.getRuntimeProviders()) { + for (PhysicsBackendRuntimeProvider provider : ImpulseBackendRegistry.getRuntimeProviders()) { LOGGER.at(Level.INFO).log("Registered physics backend runtime %s", provider.getId()); } - if (Impulse.getRuntimeProviders().isEmpty()) { + if (ImpulseBackendRegistry.getRuntimeProviders().isEmpty()) { throw new IllegalStateException("No physics backends discovered"); } - defaultBackendId = selectDefaultRuntimeProviderId(Impulse.getRuntimeProviders()); + defaultBackendId = selectDefaultRuntimeProviderId(ImpulseBackendRegistry.getRuntimeProviders()); if (defaultBackendId != null) { LOGGER.at(Level.INFO).log("Using default physics backend %s", defaultBackendId); return; @@ -247,35 +109,10 @@ static BackendId selectDefaultRuntimeProviderId( return providers.iterator().next().getId(); } - static int configuredPositiveInt(@Nonnull String property, - int defaultValue) { - return configuredPositiveIntDetails(property, defaultValue).value(); - } - - @Nonnull - static ConfiguredPositiveInt configuredPositiveIntDetails(@Nonnull String property, - int defaultValue) { - if (defaultValue < 1) { - throw new IllegalArgumentException("defaultValue must be positive"); - } - String configured = System.getProperty(property); - if (configured == null || configured.isBlank()) { - return new ConfiguredPositiveInt(defaultValue, configured, false); - } - try { - int parsed = Integer.parseInt(configured.trim()); - return parsed > 0 - ? new ConfiguredPositiveInt(parsed, configured, false) - : new ConfiguredPositiveInt(defaultValue, configured, true); - } catch (NumberFormatException exception) { - return new ConfiguredPositiveInt(defaultValue, configured, true); - } - } - @Nonnull private String getAvailableBackendIds() { StringBuilder ids = new StringBuilder(); - for (PhysicsBackendRuntimeProvider backend : Impulse.getRuntimeProviders()) { + for (PhysicsBackendRuntimeProvider backend : ImpulseBackendRegistry.getRuntimeProviders()) { if (!ids.isEmpty()) { ids.append(", "); } @@ -284,125 +121,9 @@ private String getAvailableBackendIds() { return ids.toString(); } - private void registerComponents() { - ComponentRegistryProxy entityRegistry = getEntityStoreRegistry(); - physicsBodyAttachmentComponentType = entityRegistry.registerComponent( - PhysicsBodyAttachmentComponent.class, - "PhysicsBodyAttachment", - PhysicsBodyAttachmentComponent.CODEC); - physicsBodyIdentityComponentType = entityRegistry.registerComponent( - PhysicsBodyIdentityComponent.class, - "PhysicsBodyIdentity", - PhysicsBodyIdentityComponent.CODEC); - physicsBodyShapeComponentType = entityRegistry.registerComponent( - PhysicsBodyShapeComponent.class, - "PhysicsBodyShape", - PhysicsBodyShapeComponent.CODEC); - physicsBodyDynamicsComponentType = entityRegistry.registerComponent( - PhysicsBodyDynamicsComponent.class, - "PhysicsBodyDynamics", - PhysicsBodyDynamicsComponent.CODEC); - physicsBodyMaterialComponentType = entityRegistry.registerComponent( - PhysicsBodyMaterialComponent.class, - "PhysicsBodyMaterial", - PhysicsBodyMaterialComponent.CODEC); - physicsBodyCollisionComponentType = entityRegistry.registerComponent( - PhysicsBodyCollisionComponent.class, - "PhysicsBodyCollision", - PhysicsBodyCollisionComponent.CODEC); - physicsBodyKinematicTargetComponentType = entityRegistry.registerComponent( - PhysicsBodyKinematicTargetComponent.class, - "PhysicsBodyKinematicTarget", - PhysicsBodyKinematicTargetComponent.CODEC); - physicsBodyLifecycleComponentType = entityRegistry.registerComponent( - PhysicsBodyLifecycleComponent.class, - "PhysicsBodyLifecycle", - PhysicsBodyLifecycleComponent.CODEC); - generatedVisualProxyComponentType = entityRegistry.registerComponent( - GeneratedVisualProxyComponent.class, - "GeneratedVisualProxy", - GeneratedVisualProxyComponent.CODEC); - physicsWorldResourceType = entityRegistry.registerResource(PhysicsWorldResource.class, - PhysicsWorldRuntimeResource::new); - physicsDebugResourceType = entityRegistry.registerResource(PhysicsDebugResource.class, - PhysicsDebugResource::new); - physicsRuntimeProfilingResourceType = entityRegistry.registerResource( - PhysicsRuntimeProfilingResource.class, - PhysicsRuntimeProfilingResource::new); - ConfiguredPositiveInt ownerPoolSize = configuredPositiveIntDetails(OWNER_POOL_SIZE_PROPERTY, - PhysicsOwnerLaneScheduler.DEFAULT_POOL_SIZE); - logOwnerPoolSize(ownerPoolSize); - physicsOwnerLaneScheduler = new PhysicsOwnerLaneScheduler( - ownerPoolSize.value(), - PhysicsOwnerLaneScheduler.DEFAULT_QUEUE_CAPACITY, - PhysicsOwnerLaneScheduler.DEFAULT_CLOSE_TIMEOUT); - physicsOwnerResourceType = entityRegistry.registerResource( - PhysicsOwnerResource.class, - physicsOwnerLaneScheduler::createLane); - persistentPhysicsWorldResourceType = entityRegistry.registerResource( - PersistentPhysicsWorldResource.class, - "PersistentPhysicsWorld", - PersistentPhysicsWorldResource.CODEC); - physicsEventFramePublishedEventType = - entityRegistry.registerWorldEventType(PhysicsEventFramePublishedEvent.class); - } - - private static void logOwnerPoolSize(@Nonnull ConfiguredPositiveInt ownerPoolSize) { - String configured = ownerPoolSize.configuredValue(); - if (configured == null || configured.isBlank()) { - LOGGER.at(Level.INFO).log("Physics owner pool size %d (default)", - ownerPoolSize.value()); - return; - } - if (ownerPoolSize.usedFallback()) { - LOGGER.at(Level.WARNING).log("Invalid %s=%s; using physics owner pool size %d", - OWNER_POOL_SIZE_PROPERTY, - configured, - ownerPoolSize.value()); - return; - } - LOGGER.at(Level.INFO).log("Physics owner pool size %d from %s=%s", - ownerPoolSize.value(), - OWNER_POOL_SIZE_PROPERTY, - configured); - } - - record ConfiguredPositiveInt(int value, - @Nullable String configuredValue, - boolean usedFallback) { - } - - private void registerSystems() { - ComponentRegistryProxy chunkRegistry = getChunkStoreRegistry(); - physicsStepSystem = new PhysicsStepSystem(); - chunkRegistry.registerSystem(physicsStepSystem); - - ComponentRegistryProxy entityRegistry = getEntityStoreRegistry(); - persistenceRestoreGroup = entityRegistry.registerSystemGroup(); - entityRegistry.registerSystem(createPhysicsOwnerLifecycleSystem()); - entityRegistry.registerSystem(new PersistentPhysicsSpaceBootstrapSystem()); - entityRegistry.registerSystem(new PersistentPhysicsBodyHydrationSystem()); - entityRegistry.registerSystem(new PersistentPhysicsJointHydrationSystem()); - entityRegistry.registerSystem(new PhysicsRuntimeHolderSystem()); - entityRegistry.registerSystem(new PhysicsBodyIdentityCleanupSystem()); - entityRegistry.registerSystem(new RigidBodyLifecycleCleanupSystem()); - entityRegistry.registerSystem(new PhysicsBodyAttachmentIndexSystem()); - entityRegistry.registerSystem(new PhysicsSyncSystem()); - entityRegistry.registerSystem(new PhysicsDebugSystem()); - entityRegistry.registerSystem(new PhysicsDetachedVisualMaterializationSystem()); - entityRegistry.registerSystem(new PhysicsSnapshotPublicationSystem()); - entityRegistry.registerSystem(new PersistentPhysicsWorldSyncSystem()); - entityRegistry.registerSystem(new RigidBodyReconciliationSystem()); - } - private void registerCommands() { CommandRegistry commandRegistry = getCommandRegistry(); - ImpulseCommandContributionRegistry.register(commandRegistry); + ImpulseCommandTreeRegistry.register(commandRegistry); } - @Nonnull - private PhysicsOwnerLifecycleSystem createPhysicsOwnerLifecycleSystem() { - physicsOwnerLifecycleSystem = new PhysicsOwnerLifecycleSystem(); - return physicsOwnerLifecycleSystem; - } } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/PhysicsComponentTypeRegistry.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/PhysicsComponentTypeRegistry.java new file mode 100644 index 00000000..430910d1 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/PhysicsComponentTypeRegistry.java @@ -0,0 +1,258 @@ +package dev.hytalemodding.impulse.core.internal; + +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.components.BodyCommandComponent; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.ExtensionSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.SolverSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.VisualMaterializationSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.VisualSyncSettingsComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionRestoreDependencyComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.CollisionLodSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.ChunkCollisionSettingsComponent; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Registered Hytale ECS component type handles for PhysicsStore entities. + */ +public final class PhysicsComponentTypeRegistry { + + @Nullable + private static ComponentType uuidComponentType; + @Nullable + private static ComponentType spaceComponentType; + @Nullable + private static ComponentType bodyComponentType; + @Nullable + private static ComponentType bodyCommandComponentType; + @Nullable + private static ComponentType chunkCollisionSourceComponentType; + @Nullable + private static ComponentType + chunkCollisionRestoreDependencyComponentType; + @Nullable + private static ComponentType chunkCollisionSettingsComponentType; + @Nullable + private static ComponentType dynamicsComponentType; + @Nullable + private static ComponentType colliderComponentType; + @Nullable + private static ComponentType shapeComponentType; + @Nullable + private static ComponentType materialComponentType; + @Nullable + private static ComponentType collisionFilterComponentType; + @Nullable + private static ComponentType jointComponentType; + @Nullable + private static ComponentType targetComponentType; + @Nullable + private static ComponentType solverSettingsComponentType; + @Nullable + private static ComponentType visualSyncSettingsComponentType; + @Nullable + private static ComponentType + visualMaterializationSettingsComponentType; + @Nullable + private static ComponentType collisionLodSettingsComponentType; + @Nullable + private static ComponentType extensionSettingsComponentType; + + private PhysicsComponentTypeRegistry() { + } + + public static void registerComponentTypes( + @Nonnull ComponentRegistryProxy registry) { + uuidComponentType = registry.registerComponent( + UuidComponent.class, + "Uuid", + UuidComponent.CODEC); + spaceComponentType = registry.registerComponent( + SpaceComponent.class, + "Space", + SpaceComponent.CODEC); + bodyComponentType = registry.registerComponent( + BodyComponent.class, + "Body", + BodyComponent.CODEC); + bodyCommandComponentType = registry.registerComponent( + BodyCommandComponent.class, + "BodyCommand", + BodyCommandComponent.CODEC); + chunkCollisionSourceComponentType = registry.registerComponent( + ChunkCollisionSourceComponent.class, + "ChunkCollisionSource", + ChunkCollisionSourceComponent.CODEC); + chunkCollisionRestoreDependencyComponentType = registry.registerComponent( + ChunkCollisionRestoreDependencyComponent.class, + "ChunkCollisionRestoreDependency", + ChunkCollisionRestoreDependencyComponent.CODEC); + chunkCollisionSettingsComponentType = registry.registerComponent( + ChunkCollisionSettingsComponent.class, + "ChunkCollisionSettings", + ChunkCollisionSettingsComponent.CODEC); + dynamicsComponentType = registry.registerComponent( + DynamicsComponent.class, + "Dynamics", + DynamicsComponent.CODEC); + colliderComponentType = registry.registerComponent( + ColliderComponent.class, + "Collider", + ColliderComponent.CODEC); + shapeComponentType = registry.registerComponent( + ShapeComponent.class, + "Shape", + ShapeComponent.CODEC); + materialComponentType = registry.registerComponent( + MaterialComponent.class, + "Material", + MaterialComponent.CODEC); + collisionFilterComponentType = registry.registerComponent( + CollisionFilterComponent.class, + "CollisionFilter", + CollisionFilterComponent.CODEC); + jointComponentType = registry.registerComponent( + JointComponent.class, + "Joint", + JointComponent.CODEC); + targetComponentType = registry.registerComponent( + TargetComponent.class, + "Target", + TargetComponent.CODEC); + solverSettingsComponentType = registry.registerComponent( + SolverSettingsComponent.class, + "SolverSettings", + SolverSettingsComponent.CODEC); + visualSyncSettingsComponentType = registry.registerComponent( + VisualSyncSettingsComponent.class, + "VisualSyncSettings", + VisualSyncSettingsComponent.CODEC); + visualMaterializationSettingsComponentType = registry.registerComponent( + VisualMaterializationSettingsComponent.class, + "VisualMaterializationSettings", + VisualMaterializationSettingsComponent.CODEC); + collisionLodSettingsComponentType = registry.registerComponent( + CollisionLodSettingsComponent.class, + "CollisionLodSettings", + CollisionLodSettingsComponent.CODEC); + extensionSettingsComponentType = registry.registerComponent( + ExtensionSettingsComponent.class, + "ExtensionSettings", + ExtensionSettingsComponent.CODEC); + } + + @Nonnull + public static ComponentType uuidComponentType() { + return uuidComponentType; + } + + @Nonnull + public static ComponentType spaceComponentType() { + return spaceComponentType; + } + + @Nonnull + public static ComponentType bodyComponentType() { + return bodyComponentType; + } + + @Nonnull + public static ComponentType bodyCommandComponentType() { + return bodyCommandComponentType; + } + + @Nonnull + public static ComponentType + chunkCollisionSourceComponentType() { + return chunkCollisionSourceComponentType; + } + + @Nonnull + public static ComponentType + chunkCollisionRestoreDependencyComponentType() { + return chunkCollisionRestoreDependencyComponentType; + } + + @Nonnull + public static ComponentType + chunkCollisionSettingsComponentType() { + return chunkCollisionSettingsComponentType; + } + + @Nonnull + public static ComponentType dynamicsComponentType() { + return dynamicsComponentType; + } + + @Nonnull + public static ComponentType colliderComponentType() { + return colliderComponentType; + } + + @Nonnull + public static ComponentType shapeComponentType() { + return shapeComponentType; + } + + @Nonnull + public static ComponentType materialComponentType() { + return materialComponentType; + } + + @Nonnull + public static ComponentType collisionFilterComponentType() { + return collisionFilterComponentType; + } + + @Nonnull + public static ComponentType jointComponentType() { + return jointComponentType; + } + + @Nonnull + public static ComponentType targetComponentType() { + return targetComponentType; + } + + @Nonnull + public static ComponentType solverSettingsComponentType() { + return solverSettingsComponentType; + } + + @Nonnull + public static ComponentType + visualSyncSettingsComponentType() { + return visualSyncSettingsComponentType; + } + + @Nonnull + public static ComponentType + visualMaterializationSettingsComponentType() { + return visualMaterializationSettingsComponentType; + } + + @Nonnull + public static ComponentType + collisionLodSettingsComponentType() { + return collisionLodSettingsComponentType; + } + + @Nonnull + public static ComponentType + extensionSettingsComponentType() { + return extensionSettingsComponentType; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/PhysicsStoreEarlyPluginProbe.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/PhysicsStoreEarlyPluginProbe.java new file mode 100644 index 00000000..c43cd1b7 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/PhysicsStoreEarlyPluginProbe.java @@ -0,0 +1,83 @@ +package dev.hytalemodding.impulse.core.internal; + +import com.hypixel.hytale.server.core.plugin.PluginBase; +import com.hypixel.hytale.server.core.universe.system.WorldConfigSaveSystem; +import com.hypixel.hytale.server.core.universe.world.World; +import dev.hytalemodding.impulse.early.PhysicsStoreWorld; +import java.lang.reflect.Method; +import javax.annotation.Nonnull; + +/** + * Reflection-only readiness check for the PhysicsStore early plugin transform. + */ +public final class PhysicsStoreEarlyPluginProbe { + + private static final String PHYSICS_STORE_CLASS = + "com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore"; + private static final String WORLD_LIFECYCLE_MARKER = + "impulse$physicsStoreLifecyclePatched"; + private static final String WORLD_RESOURCE_SAVE_MARKER = + "impulse$physicsStoreResourceSavePatched"; + + private PhysicsStoreEarlyPluginProbe() { + } + + public static void requireAvailable() { + Class physicsStoreClass = requireClass(PHYSICS_STORE_CLASS); + requireMethod(PluginBase.class, "getPhysicsStoreRegistry"); + Method worldAccessor = requireMethod(World.class, "getPhysicsStore"); + if (!worldAccessor.getReturnType().equals(physicsStoreClass)) { + throw new IllegalStateException("Impulse PhysicsStore early plugin installed an " + + "unexpected World.getPhysicsStore() return type: " + + worldAccessor.getReturnType().getName()); + } + if (!PhysicsStoreWorld.class.isAssignableFrom(World.class)) { + throw new IllegalStateException("Impulse PhysicsStore early plugin did not make World " + + "implement " + PhysicsStoreWorld.class.getName()); + } + requireField(World.class, WORLD_LIFECYCLE_MARKER); + requireField(WorldConfigSaveSystem.class, WORLD_RESOURCE_SAVE_MARKER); + } + + public static boolean isAvailable() { + try { + requireAvailable(); + return true; + } catch (IllegalStateException exception) { + return false; + } + } + + @Nonnull + private static Class requireClass(@Nonnull String className) { + try { + return Class.forName(className); + } catch (ClassNotFoundException exception) { + throw unavailable(exception); + } + } + + @Nonnull + private static Method requireMethod(@Nonnull Class owner, @Nonnull String methodName) { + try { + return owner.getMethod(methodName); + } catch (NoSuchMethodException exception) { + throw unavailable(exception); + } + } + + private static void requireField(@Nonnull Class owner, @Nonnull String fieldName) { + try { + owner.getDeclaredField(fieldName); + } catch (NoSuchFieldException exception) { + throw unavailable(exception); + } + } + + @Nonnull + private static IllegalStateException unavailable(@Nonnull ReflectiveOperationException cause) { + return new IllegalStateException("Impulse requires the PhysicsStore early plugin. " + + "Install impulse-early-plugin as a Hytale early plugin and start the server with " + + "--accept-early-plugins.", cause); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/PhysicsStoreRegistration.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/PhysicsStoreRegistration.java new file mode 100644 index 00000000..fe71637c --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/PhysicsStoreRegistration.java @@ -0,0 +1,234 @@ +package dev.hytalemodding.impulse.core.internal; + +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.plugin.PluginBase; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.early.PhysicsStoreHooks; +import dev.hytalemodding.impulse.core.internal.persistence.PhysicsStoreHolderStorage; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsStoreCleanupHooks; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsEventResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsProfilingResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStepSchedulerResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStepSchedulerResource.TickDecision; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStoreReadQueueResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldSettingsResource; +import dev.hytalemodding.impulse.core.internal.systems.binding.BodyBindingSystem; +import dev.hytalemodding.impulse.core.internal.systems.BodyCommandApplicationSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.ColliderBindingSystem; +import dev.hytalemodding.impulse.core.internal.systems.publication.CompletedStepPublicationSystem; +import dev.hytalemodding.impulse.core.internal.systems.IdentityIndexSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.JointBindingSystem; +import dev.hytalemodding.impulse.core.internal.systems.PersistenceHydrationSystem; +import dev.hytalemodding.impulse.core.internal.systems.PhysicsStoreQueuedReadSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.SpaceBindingSystem; +import dev.hytalemodding.impulse.core.internal.systems.SpaceSettingsApplicationSystem; +import dev.hytalemodding.impulse.core.internal.systems.step.StepSubmissionSystem; +import dev.hytalemodding.impulse.core.internal.systems.StaleBodyRemovalSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.TargetBindingSystem; +import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Registers authoritative PhysicsStore ECS types after the early plugin has patched Hytale. + */ +public final class PhysicsStoreRegistration { + + private static final String REGISTRY_METHOD = "getPhysicsStoreRegistry"; + @Nonnull + private static final Consumer SHUTDOWN_CLEANUP = + PhysicsStoreRegistration::clearRuntimeStateBeforeShutdown; + @Nonnull + private static final Function> HOLDER_SAVE_HOOK = + PhysicsStoreHolderStorage::save; + @Nonnull + private static final PhysicsStoreHooks.TickGate STEP_TICK_GATE = + PhysicsStoreRegistration::shouldTickPhysicsStore; + + private PhysicsStoreRegistration() { + } + + public static void register(@Nonnull ComponentRegistryProxy registry) { + PhysicsStoreHooks.registerShutdownHook(SHUTDOWN_CLEANUP); + PhysicsStoreHooks.registerSaveHook(HOLDER_SAVE_HOOK); + PhysicsStoreHooks.registerTickGate(STEP_TICK_GATE); + + PhysicsResourceTypes.registerResourceTypes(registry); + + registry.registerSystem(new PersistenceHydrationSystem()); + registry.registerSystem(new IdentityIndexSystem()); + registry.registerSystem(new SpaceBindingSystem()); + registry.registerSystem(new SpaceSettingsApplicationSystem()); + registry.registerSystem(new BodyBindingSystem()); + registry.registerSystem(new ColliderBindingSystem()); + registry.registerSystem(new JointBindingSystem()); + registry.registerSystem(new StaleBodyRemovalSystem()); + registry.registerSystem(new BodyCommandApplicationSystem()); + registry.registerSystem(new TargetBindingSystem()); + registry.registerSystem(new CompletedStepPublicationSystem()); + registry.registerSystem(new PhysicsStoreQueuedReadSystem()); + registry.registerSystem(new StepSubmissionSystem()); + } + + private static void clearRuntimeStateBeforeShutdown(@Nonnull PhysicsStore physicsStore) { + Store store = physicsStore.getStore(); + if (store.isShutdown()) { + return; + } + RuntimeException failure = null; + failure = runShutdownCleanup(failure, + () -> PhysicsStoreHolderStorage.save(physicsStore).join()); + failure = runShutdownCleanup(failure, + () -> cleanupResource(store, + PhysicsStepSchedulerResource.getResourceType(), + PhysicsStepSchedulerResource::close)); + failure = runShutdownCleanup(failure, + () -> cleanupResource(store, + PhysicsRuntimeResource.getResourceType(), + PhysicsRuntimeResource::destroyBackendBindings)); + failure = runShutdownCleanup(failure, + () -> PhysicsStoreCleanupHooks.clearFullStoreRuntimeResources(store)); + failure = runShutdownCleanup(failure, + () -> store.getExternalData().clearUuidIndex()); + failure = runShutdownCleanup(failure, + () -> cleanupResource(store, + PhysicsSpaceCompatibilityIndexResource.getResourceType(), + PhysicsSpaceCompatibilityIndexResource::clear)); + failure = runShutdownCleanup(failure, + () -> cleanupResource(store, + PhysicsSnapshotResource.getResourceType(), + PhysicsSnapshotResource::clear)); + failure = runShutdownCleanup(failure, + () -> cleanupResource(store, + PhysicsEventResource.getResourceType(), + PhysicsEventResource::clear)); + failure = runShutdownCleanup(failure, + () -> cleanupResource(store, + PhysicsProfilingResource.getResourceType(), + PhysicsProfilingResource::reset)); + failure = runShutdownCleanup(failure, + () -> cleanupResource(store, + PhysicsStoreReadQueueResource.getResourceType(), + PhysicsStoreReadQueueResource::clear)); + if (failure != null) { + throw failure; + } + } + + private static > void cleanupResource( + @Nonnull Store store, + @Nonnull ResourceType type, + @Nonnull Consumer cleanup) { + T resource = store.getResource(type); + if (resource == null) { + return; + } + cleanup.accept(resource); + } + + private static boolean shouldTickPhysicsStore(@Nonnull PhysicsStore physicsStore, float dt) { + Store store = physicsStore.getStore(); + if (store.isShutdown()) { + return true; + } + PhysicsWorldSettings settings = store.getResource(PhysicsWorldSettingsResource.getResourceType()) + .getSettings(); + TickDecision decision = store.getResource(PhysicsStepSchedulerResource.getResourceType()) + .beforeStoreTick(dt, + settings.getStepSchedulingMode(), + maxSubmittedDtSeconds(settings), + System.nanoTime()); + if (decision.shouldTick()) { + return true; + } + recordPendingStepSkip(store, decision); + return false; + } + + private static void recordPendingStepSkip(@Nonnull Store store, + @Nonnull TickDecision decision) { + Store entityStore = store.getExternalData() + .getWorld() + .getEntityStore() + .getStore(); + if (entityStore.isShutdown()) { + return; + } + PhysicsRuntimeProfilingResource profiling = entityStore.getResource( + PhysicsRuntimeProfilingResource.getResourceType()); + if (!profiling.isEnabled()) { + return; + } + profiling.recordStepSkippedPending(decision.pendingStepAgeNanos()); + profiling.recordStepScheduling(decision.inputDtSeconds(), + decision.submittedDtSeconds(), + decision.backlogDtSeconds(), + decision.droppedBacklogDtSeconds(), + decision.dtCapHit()); + } + + private static float maxSubmittedDtSeconds(@Nonnull PhysicsWorldSettings settings) { + float maxStepDt = settings.getMaxStepDt() > 0.0f + ? settings.getMaxStepDt() + : PhysicsWorldSettings.DEFAULT_MAX_STEP_DT; + int maxSteps = switch (settings.getStepMode()) { + case FIXED, CCD -> settings.getSimulationSteps(); + case ADAPTIVE, PROGRESSIVE_REFINEMENT -> PhysicsWorldSettings.MAX_SIMULATION_STEPS; + }; + return maxStepDt * maxSteps; + } + + @Nullable + private static RuntimeException runShutdownCleanup(@Nullable RuntimeException failure, + @Nonnull Runnable cleanup) { + try { + cleanup.run(); + return failure; + } catch (RuntimeException exception) { + if (failure == null) { + return exception; + } + failure.addSuppressed(exception); + return failure; + } + } + + @Nonnull + @SuppressWarnings("unchecked") + public static ComponentRegistryProxy physicsStoreRegistry( + @Nonnull PluginBase plugin) { + try { + Method method = plugin.getClass().getMethod(REGISTRY_METHOD); + return (ComponentRegistryProxy) method.invoke(plugin); + } catch (NoSuchMethodException exception) { + throw new IllegalStateException("Impulse requires the PhysicsStore early plugin to " + + "patch PluginBase." + REGISTRY_METHOD + "()", exception); + } catch (IllegalAccessException exception) { + throw new IllegalStateException("Cannot access patched PhysicsStore registry method", + exception); + } catch (InvocationTargetException exception) { + Throwable cause = exception.getCause(); + if (cause instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (cause instanceof Error error) { + throw error; + } + throw new IllegalStateException("PhysicsStore registry method failed", cause); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/CleanCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/CleanCommand.java index 756d34e2..194ca12b 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/CleanCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/CleanCommand.java @@ -1,10 +1,6 @@ package dev.hytalemodding.impulse.core.internal.commands; -import com.hypixel.hytale.component.ArchetypeChunk; -import com.hypixel.hytale.component.CommandBuffer; -import com.hypixel.hytale.component.ComponentType; import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.RemoveReason; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.command.system.CommandContext; @@ -14,17 +10,21 @@ import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.components.GeneratedVisualProxyComponent; -import dev.hytalemodding.impulse.core.internal.modules.control.components.PhysicsControlSessionComponent; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.PhysicsEntityProjectionCleanup; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.PhysicsEntityProjectionCleanup.Result; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsTopologyMutations; import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResetResult; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.modules.control.systems.PhysicsControlSessionCleanup; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollision; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsCleanupHooks; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import java.util.Set; -import java.util.concurrent.atomic.AtomicIntegerArray; +import java.util.UUID; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.joml.Vector3d; @@ -33,22 +33,18 @@ /** * Clears Impulse-owned runtime state from the target world. * - *

This removes Hytale adapter entities, visual proxies, runtime bodies, joints, - * and current world-collision cache bodies. Explicit physics spaces are kept, including - * their world-collision settings. Spaces with streaming world collision enabled may - * build fresh backend terrain bodies again on the next streaming tick.

+ *

This removes Impulse-owned visual entities, detaches external physics attachments, + * clears runtime bodies, joints, and current PhysicsChunk collision cache bodies. Explicit + * physics spaces are kept, including their PhysicsChunk collision settings. Spaces with streaming + * PhysicsChunk collision enabled may build fresh backend terrain bodies again on the next + * streaming tick.

* *

When a radius is provided, cleanup is intentionally narrower: it selects * registered body snapshots near the player, removes those bodies and their - * attachments/proxies, and leaves spaces plus the world-collision cache intact.

+ * attachments/proxies, and leaves spaces plus the PhysicsChunk collision cache intact.

*/ public class CleanCommand extends AbstractWorldCommand { - private static final int REMOVED_BODY_ENTITIES = 0; - private static final int REMOVED_ORPHAN_VISUAL_ENTITIES = 1; - private static final int REMOVED_SESSIONS = 2; - private static final int REMOVED_ENTITY_COUNTERS = 3; - private final OptionalArg radiusArg = this.withOptionalArg( "radius", "Only clean Impulse bodies and visual entities within this radius of the player", @@ -73,50 +69,83 @@ protected void execute(@Nonnull CommandContext context, private static void cleanAll(@Nonnull CommandContext context, @Nonnull World world, @Nonnull Store store) { - ComponentType attachmentType = - PhysicsBodyAttachmentComponent.getComponentType(); - ComponentType generatedProxyType = - GeneratedVisualProxyComponent.getComponentType(); - AtomicIntegerArray removedEntities = new AtomicIntegerArray(REMOVED_ENTITY_COUNTERS); - store.forEachEntityParallel(attachmentType, - (index, archetypeChunk, commandBuffer) -> { - removedEntities.incrementAndGet(REMOVED_BODY_ENTITIES); - commandBuffer.removeEntity(archetypeChunk.getReferenceTo(index), RemoveReason.REMOVE); - }); - - store.forEachEntityParallel(generatedProxyType, - (index, archetypeChunk, commandBuffer) -> { - if (archetypeChunk.getComponent(index, attachmentType) != null) { - return; - } - - removedEntities.incrementAndGet(REMOVED_ORPHAN_VISUAL_ENTITIES); - commandBuffer.removeEntity(archetypeChunk.getReferenceTo(index), RemoveReason.REMOVE); - }); + Result projectionCleanup = + PhysicsEntityProjectionCleanup.cleanAll(store, + PhysicsCleanupHooks.detachableEntityMarkers()); + int removedPluginEntities = PhysicsCleanupHooks.cleanupEntityStore(store); + + CompletionStage reset = + PhysicsThreading.callWhenBackendIdleOnWorldThread(world, + "clear PhysicsStore body entities", + PhysicsTopologyMutations::clearBodiesKeepingSpaces); + reset.whenComplete((result, failure) -> sendCleanAllResult(world, + context, + projectionCleanup, + removedPluginEntities, + result, + failure)); + } - ComponentType controlSessionType = - controlSessionTypeOrNull(); - if (controlSessionType != null) { - store.forEachEntityParallel(controlSessionType, - (index, archetypeChunk, commandBuffer) -> { - removedEntities.incrementAndGet(REMOVED_SESSIONS); - commandBuffer.removeComponent(archetypeChunk.getReferenceTo(index), - controlSessionType); - }); + private static void sendCleanAllResult(@Nonnull World world, + @Nonnull CommandContext context, + @Nonnull Result projectionCleanup, + int removedSessions, + @Nullable PhysicsRuntimeResetResult reset, + @Nullable Throwable failure) { + Runnable sender = () -> { + if (failure != null) { + Throwable cause = unwrap(failure); + String message = cause.getMessage() != null ? cause.getMessage() : cause.toString(); + context.sendMessage(Message.raw("Failed to clean Impulse physics runtime state: " + + message)); + return; + } + if (reset == null) { + context.sendMessage(Message.raw("Failed to clean Impulse physics runtime state.")); + return; + } + sendCleanAllSuccess(context, + projectionCleanup, + removedSessions, + reset, + world.getName()); + }; + if (world.isInThread()) { + sender.run(); + return; } + world.execute(sender); + } - PhysicsWorldRuntimeResource resource = PhysicsWorldRuntimeResource.require(store); - PhysicsRuntimeResetResult reset = - resource.resetRuntimeStateKeepingSpaces(world.getName()); - - context.sendMessage(Message.raw("Removed " + removedEntities.get(REMOVED_BODY_ENTITIES) - + " Impulse attachment entities, " + removedEntities.get(REMOVED_ORPHAN_VISUAL_ENTITIES) - + " orphan visual proxy entities, " + reset.removedBodies() + " runtime bodies, " - + reset.removedJoints() + " joints, and " - + removedEntities.get(REMOVED_SESSIONS) + " control sessions in world " + world.getName() + private static void sendCleanAllSuccess(@Nonnull CommandContext context, + @Nonnull Result projectionCleanup, + int removedSessions, + @Nonnull PhysicsRuntimeResetResult reset, + @Nonnull String worldName) { + String prefix = projectionCleanup.skipped() + ? "Impulse PhysicsEntity integration is not available; skipped EntityStore attachment/proxy cleanup. " + : ""; + context.sendMessage(Message.raw(prefix + "Removed " + + projectionCleanup.removedAttachmentEntities() + + " Impulse-owned attachment entities, " + + projectionCleanup.detachedExternalAttachments() + + " detached external attachments, " + + projectionCleanup.removedOrphanVisualEntities() + + " orphan visual proxy entities, " + reset.removedBodies() + + " runtime bodies, " + reset.removedJoints() + " joints, and " + + removedSessions + " control sessions in world " + worldName + ". Kept " + reset.keptSpaces() + " explicit physics spaces.")); } + @Nonnull + private static Throwable unwrap(@Nonnull Throwable failure) { + if (failure instanceof CompletionException completionException + && completionException.getCause() != null) { + return completionException.getCause(); + } + return failure; + } + private void cleanWithinRadius(@Nonnull CommandContext context, @Nonnull World world, @Nonnull Store store) { @@ -136,77 +165,94 @@ private void cleanWithinRadius(@Nonnull CommandContext context, return; } - PhysicsWorldRuntimeResource resource = PhysicsWorldRuntimeResource.require(store); - resource.refreshBodySnapshots(); - Set selectedBodyKeys = selectBodyKeysNear(resource, center, radius); + Store physicsStore = PhysicsThreading.store(world); + SelectedBodies selectedBodies = selectBodiesNear(physicsStore, center, radius); double radiusSquared = (double) radius * radius; - ComponentType attachmentType = - PhysicsBodyAttachmentComponent.getComponentType(); - ComponentType generatedProxyType = - GeneratedVisualProxyComponent.getComponentType(); - - AtomicIntegerArray removedEntities = new AtomicIntegerArray(REMOVED_ENTITY_COUNTERS); - store.forEachEntityParallel(attachmentType, - (index, archetypeChunk, commandBuffer) -> { - PhysicsBodyAttachmentComponent attachment = - archetypeChunk.getComponent(index, attachmentType); - assert attachment != null; - if (!selectedBodyKeys.contains(attachment.getBodyKey())) { - return; - } - - removedEntities.incrementAndGet(REMOVED_BODY_ENTITIES); - commandBuffer.removeEntity(archetypeChunk.getReferenceTo(index), RemoveReason.REMOVE); - }); - - store.forEachEntityParallel(generatedProxyType, - (index, archetypeChunk, commandBuffer) -> { - if (archetypeChunk.getComponent(index, attachmentType) != null - || !entityWithinRadius(archetypeChunk, index, center, radiusSquared)) { - return; - } - - removedEntities.incrementAndGet(REMOVED_ORPHAN_VISUAL_ENTITIES); - commandBuffer.removeEntity(archetypeChunk.getReferenceTo(index), RemoveReason.REMOVE); - }); - - ComponentType controlSessionType = - controlSessionTypeOrNull(); - if (controlSessionType != null) { - store.forEachEntityParallel(controlSessionType, - (index, archetypeChunk, commandBuffer) -> { - PhysicsControlSessionComponent session = - archetypeChunk.getComponent(index, controlSessionType); - assert session != null; - if (!controlSessionSelected(commandBuffer, - archetypeChunk, - index, - session, - selectedBodyKeys, - center, - radiusSquared)) { - return; - } + CompletionStage clean = PhysicsThreading.callWhenBackendIdleOnWorldThread(world, + "clean Impulse physics bodies within radius", + backendStore -> cleanSelectedBodies(store, + backendStore, + selectedBodies, + center, + radiusSquared)); + clean.whenComplete((result, failure) -> sendCleanRadiusResult(world, + context, + radius, + result, + failure)); + } - removedEntities.incrementAndGet(REMOVED_SESSIONS); - PhysicsControlSessionCleanup.cleanup(store, session); - commandBuffer.removeComponent(archetypeChunk.getReferenceTo(index), - controlSessionType); - }); - } + @Nonnull + private static RadiusCleanResult cleanSelectedBodies(@Nonnull Store store, + @Nonnull Store physicsStore, + @Nonnull SelectedBodies selectedBodies, + @Nonnull Vector3d center, + double radiusSquared) { + Result projectionCleanup = PhysicsEntityProjectionCleanup.cleanSelected(store, + selectedBodies.bodyUuids(), + center, + radiusSquared, + PhysicsCleanupHooks.detachableEntityMarkers()); + int removedPluginEntities = PhysicsCleanupHooks.cleanupSelectedEntityStore(store, + selectedBodies.bodyUuids(), + center, + radiusSquared); int removedBodies = 0; - for (RigidBodyKey bodyKey : selectedBodyKeys) { - resource.destroyBody(bodyKey); + for (UUID bodyUuid : selectedBodies.bodyUuids()) { + PhysicsBodies.destroy(physicsStore, bodyUuid); removedBodies++; } - context.sendMessage(Message.raw("Removed " + removedEntities.get(REMOVED_BODY_ENTITIES) - + " Impulse attachment entities, " + removedEntities.get(REMOVED_ORPHAN_VISUAL_ENTITIES) - + " orphan visual proxy entities, " + removedBodies - + " runtime bodies, and " + removedEntities.get(REMOVED_SESSIONS) - + " control sessions within radius " + radius + " in world " + world.getName() - + ". Kept explicit physics spaces and world-collision cache.")); + return new RadiusCleanResult(projectionCleanup, + removedPluginEntities, + removedBodies); + } + + private static void sendCleanRadiusResult(@Nonnull World world, + @Nonnull CommandContext context, + float radius, + @Nullable RadiusCleanResult result, + @Nullable Throwable failure) { + Runnable sender = () -> { + if (failure != null) { + Throwable cause = unwrap(failure); + String message = cause.getMessage() != null ? cause.getMessage() : cause.toString(); + context.sendMessage(Message.raw("Failed to clean Impulse physics bodies within radius: " + + message)); + return; + } + if (result == null) { + context.sendMessage(Message.raw("Failed to clean Impulse physics bodies within radius.")); + return; + } + sendCleanRadiusSuccess(context, result, radius, world.getName()); + }; + if (world.isInThread()) { + sender.run(); + return; + } + world.execute(sender); + } + + private static void sendCleanRadiusSuccess(@Nonnull CommandContext context, + @Nonnull RadiusCleanResult result, + float radius, + @Nonnull String worldName) { + Result projectionCleanup = result.projectionCleanup(); + String prefix = projectionCleanup.skipped() + ? "Impulse PhysicsEntity integration is not available; skipped EntityStore attachment/proxy cleanup. " + : ""; + context.sendMessage(Message.raw(prefix + "Removed " + + projectionCleanup.removedAttachmentEntities() + + " Impulse-owned attachment entities, " + + projectionCleanup.detachedExternalAttachments() + + " detached external attachments, " + + projectionCleanup.removedOrphanVisualEntities() + " orphan visual proxy entities, " + + result.removedBodies() + " runtime bodies, and " + + result.removedSessions() + + " control sessions within radius " + radius + " in world " + worldName + + ". Kept explicit physics spaces and PhysicsChunk collision cache.")); } @Nullable @@ -220,65 +266,33 @@ private static Vector3d playerPosition(@Nonnull CommandContext context, } @Nonnull - private static Set selectBodyKeysNear(@Nonnull PhysicsWorldRuntimeResource resource, + private static SelectedBodies selectBodiesNear(@Nonnull Store store, @Nonnull Vector3d center, float radius) { - Set bodyKeys = new ObjectOpenHashSet<>(); - Vector3f centerF = new Vector3f((float) center.x, (float) center.y, (float) center.z); - for (SpaceId spaceId : resource.getSpaceIds()) { - resource.forEachIndexedBodySnapshotNear(spaceId, - centerF, - radius, - (bodyKey, snapshot, bodySpaceId, kind, persistenceMode) -> bodyKeys.add(bodyKey)); - } - return bodyKeys; - } - - private static boolean controlSessionSelected( - @Nonnull CommandBuffer commandBuffer, - @Nonnull ArchetypeChunk archetypeChunk, - int index, - @Nonnull PhysicsControlSessionComponent session, - @Nonnull Set selectedBodyKeys, - @Nonnull Vector3d center, - double radiusSquared) { - if (containsBody(selectedBodyKeys, session.getBodyKey()) - || containsBody(selectedBodyKeys, session.getAnchorBodyKey()) - || entityWithinRadius(archetypeChunk, index, center, radiusSquared)) { - return true; - } - - Ref targetRef = session.getTargetRef(); - if (targetRef == null || !targetRef.isValid()) { - return false; + Set bodyUuids = new ObjectOpenHashSet<>(); + double radiusSquared = (double) radius * radius; + for (PhysicsBodySnapshot snapshot : PhysicsBodies.snapshotFrame(store).bodies()) { + if (!PhysicsBodies.isRegistered(store, snapshot.bodyUuid()) + || PhysicsChunkCollision.isChunkCollisionBody(store, snapshot.bodyUuid())) { + continue; + } + Vector3f position = snapshot.position(); + double dx = position.x - center.x; + double dy = position.y - center.y; + double dz = position.z - center.z; + if (dx * dx + dy * dy + dz * dz <= radiusSquared) { + bodyUuids.add(snapshot.bodyUuid()); + } } - - TransformComponent targetTransform = - commandBuffer.getComponent(targetRef, TransformComponent.getComponentType()); - return targetTransform != null && positionWithinRadius(targetTransform.getPosition(), - center, - radiusSquared); + return new SelectedBodies(bodyUuids); } - @Nullable - private static ComponentType controlSessionTypeOrNull() { - return PhysicsControlSessionComponent.isComponentTypeRegistered() - ? PhysicsControlSessionComponent.getComponentType() - : null; + private record SelectedBodies(@Nonnull Set bodyUuids) { } - private static boolean containsBody(@Nonnull Set bodyKeys, - @Nullable RigidBodyKey bodyKey) { - return bodyKey != null && bodyKeys.contains(bodyKey); - } - - private static boolean entityWithinRadius(@Nonnull ArchetypeChunk archetypeChunk, - int index, - @Nonnull Vector3d center, - double radiusSquared) { - TransformComponent transform = - archetypeChunk.getComponent(index, TransformComponent.getComponentType()); - return transform != null && positionWithinRadius(transform.getPosition(), center, radiusSquared); + private record RadiusCleanResult(@Nonnull Result projectionCleanup, + int removedSessions, + int removedBodies) { } private static boolean positionWithinRadius(@Nonnull Vector3d position, diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseCommand.java index c82dd81f..f061e851 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseCommand.java @@ -6,31 +6,28 @@ import dev.hytalemodding.impulse.core.internal.commands.debug.DebugCommand; import dev.hytalemodding.impulse.core.internal.commands.perf.PerfCommand; import dev.hytalemodding.impulse.core.internal.commands.settings.SettingsCommand; +import dev.hytalemodding.impulse.core.internal.commands.space.SpaceCommand; import java.util.Collection; -import java.util.List; import javax.annotation.Nonnull; public class ImpulseCommand extends AbstractCommandCollection { - public ImpulseCommand() { - this(List.of()); - } - - ImpulseCommand(@Nonnull Collection settingsContributions) { + ImpulseCommand(@Nonnull Collection debugCommands, + @Nonnull Collection settingsCommands) { super("impulse", "Impulse runtime commands"); addSubCommand(new BackendCommand()); addSubCommand(new CleanCommand()); - addSubCommand(new DebugCommand()); + addSubCommand(new DebugCommand(debugCommands)); addSubCommand(new PerfCommand()); SettingsCommand settingsCommand = new SettingsCommand(); - for (AbstractCommand command : settingsContributions) { - settingsCommand.addContribution(command); + for (AbstractCommand command : settingsCommands) { + settingsCommand.registerSettingsCommand(command); } addSubCommand(settingsCommand); addSubCommand(new SpaceCommand()); } - void addRootContribution(@Nonnull AbstractCommand command) { + void registerRootCommand(@Nonnull AbstractCommand command) { addSubCommand(command); } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseCommandContributionRegistry.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseCommandTreeRegistry.java similarity index 57% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseCommandContributionRegistry.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseCommandTreeRegistry.java index f57bf23b..cb3cd29b 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseCommandContributionRegistry.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseCommandTreeRegistry.java @@ -15,14 +15,16 @@ import javax.annotation.Nullable; /** - * Central contribution point for the sealed {@code /impulse} command tree. + * Central registry for the sealed {@code /impulse} command tree. */ -public final class ImpulseCommandContributionRegistry { +public final class ImpulseCommandTreeRegistry { private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); - private static final Map> ROOT_CONTRIBUTIONS = + private static final Map> ROOT_COMMANDS = new LinkedHashMap<>(); - private static final Map> SETTINGS_CONTRIBUTIONS = + private static final Map> DEBUG_COMMANDS = + new LinkedHashMap<>(); + private static final Map> SETTINGS_COMMANDS = new LinkedHashMap<>(); @Nullable @@ -30,7 +32,7 @@ public final class ImpulseCommandContributionRegistry { @Nullable private static CommandRegistration commandRegistration; - private ImpulseCommandContributionRegistry() { + private ImpulseCommandTreeRegistry() { } public static synchronized void register(@Nonnull CommandRegistry registry) { @@ -46,52 +48,67 @@ public static synchronized void unregister() { commandRegistry = null; } - public static synchronized void addRootSubCommand(@Nonnull String id, + public static synchronized void registerRootSubCommand(@Nonnull String id, @Nonnull Supplier supplier) { - if (ROOT_CONTRIBUTIONS.containsKey(id)) { + if (ROOT_COMMANDS.containsKey(id)) { return; } - ROOT_CONTRIBUTIONS.put(id, Objects.requireNonNull(supplier, "supplier")); + ROOT_COMMANDS.put(id, Objects.requireNonNull(supplier, "supplier")); rebuildIfRegistered(); } - public static synchronized void addRootAndSettingsSubCommands( + public static synchronized void registerRootAndSettingsSubCommands( @Nonnull String rootId, @Nonnull Supplier rootSupplier, @Nonnull String settingsId, @Nonnull Supplier settingsSupplier) { - boolean changed = addContribution(ROOT_CONTRIBUTIONS, rootId, rootSupplier); - changed |= addContribution(SETTINGS_CONTRIBUTIONS, settingsId, settingsSupplier); + boolean changed = addCommand(ROOT_COMMANDS, rootId, rootSupplier); + changed |= addCommand(SETTINGS_COMMANDS, settingsId, settingsSupplier); if (changed) { rebuildIfRegistered(); } } - public static synchronized void removeRootSubCommand(@Nonnull String id) { - if (ROOT_CONTRIBUTIONS.remove(id) != null) { + public static synchronized void unregisterRootSubCommand(@Nonnull String id) { + if (ROOT_COMMANDS.remove(id) != null) { + rebuildIfRegistered(); + } + } + + public static synchronized void registerDebugSubCommand(@Nonnull String id, + @Nonnull Supplier supplier) { + if (DEBUG_COMMANDS.containsKey(id)) { + return; + } + DEBUG_COMMANDS.put(id, Objects.requireNonNull(supplier, "supplier")); + rebuildIfRegistered(); + } + + public static synchronized void unregisterDebugSubCommand(@Nonnull String id) { + if (DEBUG_COMMANDS.remove(id) != null) { rebuildIfRegistered(); } } - public static synchronized void addSettingsSubCommand(@Nonnull String id, + public static synchronized void registerSettingsSubCommand(@Nonnull String id, @Nonnull Supplier supplier) { - if (SETTINGS_CONTRIBUTIONS.containsKey(id)) { + if (SETTINGS_COMMANDS.containsKey(id)) { return; } - SETTINGS_CONTRIBUTIONS.put(id, Objects.requireNonNull(supplier, "supplier")); + SETTINGS_COMMANDS.put(id, Objects.requireNonNull(supplier, "supplier")); rebuildIfRegistered(); } - public static synchronized void removeSettingsSubCommand(@Nonnull String id) { - if (SETTINGS_CONTRIBUTIONS.remove(id) != null) { + public static synchronized void unregisterSettingsSubCommand(@Nonnull String id) { + if (SETTINGS_COMMANDS.remove(id) != null) { rebuildIfRegistered(); } } - public static synchronized void removeRootAndSettingsSubCommands(@Nonnull String rootId, + public static synchronized void unregisterRootAndSettingsSubCommands(@Nonnull String rootId, @Nonnull String settingsId) { - boolean changed = ROOT_CONTRIBUTIONS.remove(rootId) != null; - changed |= SETTINGS_CONTRIBUTIONS.remove(settingsId) != null; + boolean changed = ROOT_COMMANDS.remove(rootId) != null; + changed |= SETTINGS_COMMANDS.remove(settingsId) != null; if (changed) { rebuildIfRegistered(); } @@ -108,8 +125,9 @@ static synchronized void resetForTests() { } commandRegistration = null; commandRegistry = null; - ROOT_CONTRIBUTIONS.clear(); - SETTINGS_CONTRIBUTIONS.clear(); + ROOT_COMMANDS.clear(); + DEBUG_COMMANDS.clear(); + SETTINGS_COMMANDS.clear(); } private static void rebuildIfRegistered() { @@ -118,14 +136,14 @@ private static void rebuildIfRegistered() { } } - private static boolean addContribution( - @Nonnull Map> contributions, + private static boolean addCommand( + @Nonnull Map> commands, @Nonnull String id, @Nonnull Supplier supplier) { - if (contributions.containsKey(id)) { + if (commands.containsKey(id)) { return false; } - contributions.put(id, Objects.requireNonNull(supplier, "supplier")); + commands.put(id, Objects.requireNonNull(supplier, "supplier")); return true; } @@ -148,13 +166,17 @@ private static void rebuildRegisteredRoot() { @Nonnull private static ImpulseCommand createRootCommand() { - List settingsContributions = new ArrayList<>(SETTINGS_CONTRIBUTIONS.size()); - for (Supplier supplier : SETTINGS_CONTRIBUTIONS.values()) { - settingsContributions.add(supplier.get()); + List debugCommands = new ArrayList<>(DEBUG_COMMANDS.size()); + for (Supplier supplier : DEBUG_COMMANDS.values()) { + debugCommands.add(supplier.get()); + } + List settingsCommands = new ArrayList<>(SETTINGS_COMMANDS.size()); + for (Supplier supplier : SETTINGS_COMMANDS.values()) { + settingsCommands.add(supplier.get()); } - ImpulseCommand command = new ImpulseCommand(settingsContributions); - for (Supplier supplier : ROOT_CONTRIBUTIONS.values()) { - command.addRootContribution(supplier.get()); + ImpulseCommand command = new ImpulseCommand(debugCommands, settingsCommands); + for (Supplier supplier : ROOT_COMMANDS.values()) { + command.registerRootCommand(supplier.get()); } return command; } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/SpaceCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/SpaceCommand.java deleted file mode 100644 index cba86155..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/SpaceCommand.java +++ /dev/null @@ -1,280 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.commands; - -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.Message; -import com.hypixel.hytale.server.core.command.system.CommandContext; -import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; -import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; -import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection; -import com.hypixel.hytale.server.core.command.system.basecommands.AbstractWorldCommand; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyRegistrationView; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.SpaceSummary; -import dev.hytalemodding.impulse.core.plugin.simulation.query.SpaceSummaryQuery; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Locale; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -public class SpaceCommand extends AbstractCommandCollection { - - public SpaceCommand() { - super("space", "Explicit physics space lifecycle commands"); - addSubCommand(new CreateCommand()); - addSubCommand(new ListCommand()); - addSubCommand(new DeleteCommand()); - } - - private static final class CreateCommand extends AbstractWorldCommand { - - private final OptionalArg backendArg = withOptionalArg( - "backend", - "Backend id, for example impulse:rapier", - ArgTypes.STRING); - private final OptionalArg worldCollisionArg = withOptionalArg( - "worldCollision", - "World collision mode: none, manual, or streaming", - ArgTypes.STRING); - private CreateCommand() { - super("create", "Create an explicit physics space", false); - } - - @Override - protected void execute(@Nonnull CommandContext context, - @Nonnull World world, - @Nonnull Store store) { - BackendId backendId = parseBackendId(context, backendArg, true); - if (backendId == null) { - return; - } - - WorldCollisionMode worldCollisionMode = worldCollisionArg.provided(context) - ? parseWorldCollisionMode(worldCollisionArg.get(context)) - : WorldCollisionMode.STREAMING; - if (worldCollisionMode == null) { - context.sendMessage(Message.raw("worldCollision must be none, manual, or streaming.")); - return; - } - - PhysicsSpaceSettings settings = worldCollisionMode == WorldCollisionMode.STREAMING - ? PhysicsSpaceSettings.streamingWorldCollision() - : PhysicsSpaceSettings.defaults(); - settings.getWorldCollisionSettings().setWorldCollisionMode(worldCollisionMode); - - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); - try { - SpaceId spaceId = resource.createSpace(backendId, - world.getName(), - settings); - context.sendMessage(Message.raw("Created physics space id=" - + spaceId.value() - + " backend=" + backendId.value() - + " worldCollision=" + worldCollisionMode.name().toLowerCase(Locale.ROOT) - + ".")); - } catch (RuntimeException exception) { - context.sendMessage(Message.raw("Failed to create physics space: " - + exception.getMessage())); - } - } - } - - private static final class ListCommand extends AbstractWorldCommand { - - private ListCommand() { - super("list", "List physics spaces in the target world", false); - } - - @Override - protected void execute(@Nonnull CommandContext context, - @Nonnull World world, - @Nonnull Store store) { - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); - List spaces = spaceSummaries(resource, null).stream() - .map(summary -> { - PhysicsSpaceSettings settings = resource.getSpaceSettings(summary.spaceId()); - return new SpaceListEntry(summary.spaceId(), - summary.backendId().value(), - summary.bodyCount(), - summary.jointCount(), - settings.getWorldCollisionSettings().getWorldCollisionMode()); - }) - .sorted(Comparator.comparingInt(entry -> entry.spaceId().value())) - .toList(); - - context.sendMessage(Message.raw("Physics spaces in world " + world.getName() + ":")); - if (spaces.isEmpty()) { - context.sendMessage(Message.raw("- ")); - return; - } - - for (SpaceListEntry space : spaces) { - context.sendMessage(Message.raw("- id=" + space.spaceId().value() - + " backend=" + space.backendId() - + " bodies=" + space.bodies() - + " joints=" + space.joints() - + " worldCollision=" - + space.worldCollisionMode().name().toLowerCase(Locale.ROOT))); - } - } - } - - private static final class DeleteCommand extends AbstractWorldCommand { - - private final OptionalArg spaceArg = withOptionalArg( - "space", - "Space id to delete", - ArgTypes.INTEGER); - - private DeleteCommand() { - super("delete", "Delete a physics space and its runtime backend state", true); - } - - @Override - protected void execute(@Nonnull CommandContext context, - @Nonnull World world, - @Nonnull Store store) { - if (!spaceArg.provided(context)) { - context.sendMessage(Message.raw("Missing space id. Example:" - + " /impulse space delete --space=1 --confirm")); - return; - } - - int rawSpaceId = spaceArg.get(context); - if (rawSpaceId <= 0) { - context.sendMessage(Message.raw("Space id must be a positive integer.")); - return; - } - - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); - SpaceId spaceId = new SpaceId(rawSpaceId); - if (!resource.hasSpace(spaceId)) { - context.sendMessage(Message.raw("No physics space id=" + rawSpaceId - + " exists in world " + world.getName() + ".")); - return; - } - - /* - * Backend-only bodies can be generated by systems such as streaming world collision. - * Those bodies belong to the space/cache lifecycle and are removed when the space is - * deleted. Registered bodies are gameplay/runtime resources addressed by RigidBodyKey, - * so they still require an explicit clean/destroy before deleting the space. - */ - int registeredBodies = countRegisteredBodies(resource, spaceId); - SpaceCounts counts = countSpaceContents(resource, spaceId); - int backendBodies = counts.bodies(); - int joints = counts.joints(); - if (registeredBodies > 0 || joints > 0) { - context.sendMessage(Message.raw("Physics space id=" + rawSpaceId - + " is not empty (" + registeredBodies + " registered bodies, " - + backendBodies + " backend bodies, " + joints + " joints)." - + " Use /impulse clean for populated worlds, then delete empty spaces.")); - return; - } - - resource.removeSpace(spaceId, world.getName()); - context.sendMessage(Message.raw("Deleted physics space id=" + rawSpaceId - + " with " + backendBodies + " backend bodies and " + joints + " joints.")); - } - } - - @Nonnull - private static SpaceCounts countSpaceContents(@Nonnull PhysicsWorldResource resource, - @Nonnull SpaceId spaceId) { - List summaries = spaceSummaries(resource, spaceId); - if (summaries.isEmpty()) { - return new SpaceCounts(0, 0); - } - - SpaceSummary summary = summaries.getFirst(); - return new SpaceCounts(summary.bodyCount(), summary.jointCount()); - } - - @Nonnull - private static List spaceSummaries(@Nonnull PhysicsWorldResource resource, - @Nullable SpaceId spaceId) { - return resource.query(new SpaceSummaryQuery(spaceId)) - .completion() - .toCompletableFuture() - .join(); - } - - private static int countRegisteredBodies(@Nonnull PhysicsWorldResource resource, - @Nonnull SpaceId spaceId) { - int count = 0; - for (PhysicsBodyRegistrationView registration : resource.getBodyRegistrationViews()) { - if (registration.spaceId().equals(spaceId)) { - count++; - } - } - return count; - } - - @Nullable - private static BackendId parseBackendId(@Nonnull CommandContext context, - @Nonnull OptionalArg backendArg, - boolean useDefaultWhenMissing) { - if (!backendArg.provided(context)) { - if (!useDefaultWhenMissing) { - return null; - } - - BackendId defaultBackendId = ImpulsePlugin.get().getDefaultBackendId(); - if (defaultBackendId != null) { - return defaultBackendId; - } - - context.sendMessage(Message.raw("Missing backend id. Multiple backends are installed; " - + "use --backend=. Available backends: " + availableBackendIds())); - return null; - } - - String rawBackendId = backendArg.get(context).trim(); - try { - return new BackendId(rawBackendId); - } catch (RuntimeException exception) { - context.sendMessage(Message.raw("Invalid backend id: " + rawBackendId)); - return null; - } - } - - @Nullable - private static WorldCollisionMode parseWorldCollisionMode(@Nonnull String value) { - return switch (value.toLowerCase(Locale.ROOT)) { - case "none", "off", "disabled" -> WorldCollisionMode.NONE; - case "manual" -> WorldCollisionMode.MANUAL; - case "streaming", "stream", "on", "enabled" -> WorldCollisionMode.STREAMING; - default -> null; - }; - } - - @Nonnull - private static String availableBackendIds() { - List backendIds = new ArrayList<>(); - for (PhysicsBackendRuntimeProvider provider : Impulse.getRuntimeProviders()) { - backendIds.add(provider.getId().value()); - } - backendIds.sort(String::compareTo); - return backendIds.isEmpty() ? "" : String.join(", ", backendIds); - } - - private record SpaceCounts(int bodies, int joints) { - } - - private record SpaceListEntry(@Nonnull SpaceId spaceId, - @Nonnull String backendId, - int bodies, - int joints, - @Nonnull WorldCollisionMode worldCollisionMode) { - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/backend/BackendListCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/backend/BackendListCommand.java index 3fa4bd1f..6786be59 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/backend/BackendListCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/backend/BackendListCommand.java @@ -8,7 +8,7 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.Impulse; +import dev.hytalemodding.impulse.api.ImpulseBackendRegistry; import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; import java.util.ArrayList; import java.util.List; @@ -29,7 +29,7 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull PlayerRef playerRef, @Nonnull World world) { List backendIds = new ArrayList<>(); - for (PhysicsBackendRuntimeProvider provider : Impulse.getRuntimeProviders()) { + for (PhysicsBackendRuntimeProvider provider : ImpulseBackendRegistry.getRuntimeProviders()) { backendIds.add(provider.getId().value()); } backendIds.sort(String::compareTo); diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/debug/DebugCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/debug/DebugCommand.java index 64137c71..3b42f62b 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/debug/DebugCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/debug/DebugCommand.java @@ -1,11 +1,14 @@ package dev.hytalemodding.impulse.core.internal.commands.debug; +import com.hypixel.hytale.server.core.command.system.AbstractCommand; import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection; import dev.hytalemodding.impulse.core.internal.resources.PhysicsDebugResource; +import java.util.Collection; +import javax.annotation.Nonnull; public class DebugCommand extends AbstractCommandCollection { - public DebugCommand() { + public DebugCommand(@Nonnull Collection moduleCommands) { super("debug", "Impulse debug rendering commands"); addSubCommand(new DebugToggleCommand()); addSubCommand(new DebugFlagCommand("shapes", "shape", @@ -20,8 +23,8 @@ public DebugCommand() { addSubCommand(new DebugFlagCommand("joints", "joint", PhysicsDebugResource::isDebugJointsEnabled, PhysicsDebugResource::setDebugJointsEnabled)); - addSubCommand(new DebugFlagCommand("world-collision", "world collision", - PhysicsDebugResource::isDebugWorldCollisionEnabled, - PhysicsDebugResource::setDebugWorldCollisionEnabled)); + for (AbstractCommand command : moduleCommands) { + addSubCommand(command); + } } } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/debug/DebugFlagCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/debug/DebugFlagCommand.java index b0d08e87..f2861507 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/debug/DebugFlagCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/debug/DebugFlagCommand.java @@ -8,19 +8,21 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.core.internal.resources.PhysicsDebugResource; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; import java.util.concurrent.CompletableFuture; import java.util.function.BiConsumer; import java.util.function.Function; import javax.annotation.Nonnull; -final class DebugFlagCommand extends AbstractAsyncPlayerCommand { +public final class DebugFlagCommand extends AbstractAsyncPlayerCommand { private final String label; private final Function getter; private final BiConsumer setter; - DebugFlagCommand(@Nonnull String name, + public DebugFlagCommand(@Nonnull String name, @Nonnull String label, @Nonnull Function getter, @Nonnull BiConsumer setter) { @@ -37,7 +39,9 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - PhysicsDebugResource resource = store.getResource(PhysicsDebugResource.getResourceType()); + Store physicsStore = PhysicsThreading.store(world); + PhysicsDebugResource resource = + physicsStore.getResource(PhysicsDebugResource.getResourceType()); boolean enabled = !getter.apply(resource); setter.accept(resource, enabled); ctx.sender().sendMessage(Message.raw("Impulse " + label + " debug " diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/debug/DebugToggleCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/debug/DebugToggleCommand.java index ebf57c0f..79467cd2 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/debug/DebugToggleCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/debug/DebugToggleCommand.java @@ -9,7 +9,7 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsDebugResource; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources.PhysicsDebugOverlayResource; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; @@ -26,10 +26,14 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - PhysicsDebugResource debug = store.getResource(PhysicsDebugResource.getResourceType()); + assert PhysicsDebugOverlayResource.getResourceType() != null; + PhysicsDebugOverlayResource debug = + store.getResource(PhysicsDebugOverlayResource.getResourceType()); boolean enabled; if (debug.removeSubscriber(playerRef.getUuid())) { enabled = false; + // The Hytale packet clears all debug shapes visible to the player; Impulse-owned + // shape tracking can narrow this once the server API exposes scoped removal. playerRef.getPacketHandler().write(new ClearDebugShapes()); } else { enabled = true; diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/perf/PerfStatsCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/perf/PerfStatsCommand.java index 5be7c9d4..338c3239 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/perf/PerfStatsCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/perf/PerfStatsCommand.java @@ -1,37 +1,34 @@ package dev.hytalemodding.impulse.core.internal.commands.perf; -import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.command.system.CommandContext; -import com.hypixel.hytale.server.core.command.system.basecommands.AbstractWorldCommand; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncWorldCommand; import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.simulation.query.PhysicsSpaceRuntimeStatsQuery; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsSpaceRuntimeStatsView; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.SpaceSummary; -import dev.hytalemodding.impulse.core.plugin.simulation.query.SpaceSummaryQuery; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsDiagnostics; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsAsync; +import dev.hytalemodding.impulse.core.plugin.physics.SpaceSummary; import java.util.List; +import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; -public class PerfStatsCommand extends AbstractWorldCommand { +public class PerfStatsCommand extends AbstractAsyncWorldCommand { public PerfStatsCommand() { super("stats", "Show Impulse per-space runtime body and contact counts"); } + @Nonnull @Override - protected void execute(@Nonnull CommandContext ctx, - @Nonnull World world, - @Nonnull Store store) { - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); - PhysicsWorldRuntimeResource runtime = PhysicsWorldRuntimeResource.require(resource); - List spaces = resource.query(new SpaceSummaryQuery(null)) - .completion() - .toCompletableFuture() - .join(); + protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, + @Nonnull World world) { + return PhysicsAsync.acceptOnWorldThread(world, + PhysicsDiagnostics.spaceSummariesAsync(world), + spaces -> sendStats(ctx, world, spaces)); + } + private static void sendStats(@Nonnull CommandContext ctx, + @Nonnull World world, + @Nonnull List spaces) { if (spaces.isEmpty()) { ctx.sender().sendMessage(Message.raw("Impulse runtime stats: no physics spaces in world " + world.getName() + ".")); @@ -42,86 +39,37 @@ protected void execute(@Nonnull CommandContext ctx, ctx.sender().sendMessage(Message.raw("Impulse runtime stats for world " + world.getName() + ": spaces=" + spaces.size())); for (SpaceSummary space : spaces) { - SpaceStats stats = SpaceStats.from(runtime.queryInternal( - new PhysicsSpaceRuntimeStatsQuery(space.spaceId())) - .toCompletableFuture() - .join()); + SpaceStats stats = SpaceStats.from(space); totals.add(stats); ctx.sender().sendMessage(Message.raw("Space " + space.spaceId().value() + " backend=" + space.backendId().value() + " bodies=" + stats.bodies - + " dynamic=" + stats.dynamicBodies - + " awake=" + stats.awakeDynamicBodies - + " sleeping=" + stats.sleepingDynamicBodies - + " static=" + stats.staticBodies - + " kinematic=" + stats.kinematicBodies - + " owned=" + stats.entityOwnedBodies - + " detached=" + stats.detachedBodies - + " planes=" + stats.planeBodies - + " raw=" + stats.rawBodies + " joints=" + stats.joints - + " contacts=" + stats.contacts)); + + " classification=unavailable" + + " contacts=unavailable")); } ctx.sender().sendMessage(Message.raw("Totals: bodies=" + totals.bodies - + " dynamic=" + totals.dynamicBodies - + " awake=" + totals.awakeDynamicBodies - + " sleeping=" + totals.sleepingDynamicBodies - + " static=" + totals.staticBodies - + " kinematic=" + totals.kinematicBodies - + " owned=" + totals.entityOwnedBodies - + " detached=" + totals.detachedBodies - + " planes=" + totals.planeBodies - + " raw=" + totals.rawBodies + " joints=" + totals.joints - + " contacts=" + totals.contacts)); + + " classification=unavailable" + + " contacts=unavailable")); } private static final class SpaceStats { private int bodies; - private int dynamicBodies; - private int awakeDynamicBodies; - private int sleepingDynamicBodies; - private int staticBodies; - private int kinematicBodies; - private int entityOwnedBodies; - private int detachedBodies; - private int planeBodies; - private int rawBodies; private int joints; - private int contacts; @Nonnull - private static SpaceStats from(@Nonnull PhysicsSpaceRuntimeStatsView view) { + private static SpaceStats from(@Nonnull SpaceSummary summary) { SpaceStats stats = new SpaceStats(); - stats.bodies = view.bodies(); - stats.dynamicBodies = view.dynamicBodies(); - stats.awakeDynamicBodies = view.awakeDynamicBodies(); - stats.sleepingDynamicBodies = view.sleepingDynamicBodies(); - stats.staticBodies = view.staticBodies(); - stats.kinematicBodies = view.kinematicBodies(); - stats.entityOwnedBodies = view.entityOwnedBodies(); - stats.detachedBodies = view.detachedBodies(); - stats.planeBodies = view.planeBodies(); - stats.rawBodies = view.rawBodies(); - stats.joints = view.joints(); - stats.contacts = view.contacts(); + stats.bodies = summary.bodyCount(); + stats.joints = summary.jointCount(); return stats; } private void add(@Nonnull SpaceStats stats) { bodies += stats.bodies; - dynamicBodies += stats.dynamicBodies; - awakeDynamicBodies += stats.awakeDynamicBodies; - sleepingDynamicBodies += stats.sleepingDynamicBodies; - staticBodies += stats.staticBodies; - kinematicBodies += stats.kinematicBodies; - entityOwnedBodies += stats.entityOwnedBodies; - detachedBodies += stats.detachedBodies; - planeBodies += stats.planeBodies; - rawBodies += stats.rawBodies; joints += stats.joints; - contacts += stats.contacts; } } } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/EventCollectionSettingCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/EventCollectionSettingCommand.java index fef23a90..b95841a0 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/EventCollectionSettingCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/EventCollectionSettingCommand.java @@ -10,8 +10,10 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsEventCollectionMode; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsWorlds; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventCollectionMode; import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; @@ -34,9 +36,10 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); + Store physicsStore = PhysicsThreading.store(world); if (!modeArg.provided(ctx)) { - PhysicsEventCollectionMode mode = resource.getWorldSettings().getEventCollectionMode(); + PhysicsEventCollectionMode mode = PhysicsWorlds.settings(physicsStore) + .getEventCollectionMode(); ctx.sender().sendMessage(Message.raw("Impulse event collection: " + mode.getSerializedName())); return CompletableFuture.completedFuture(null); @@ -51,9 +54,9 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, return CompletableFuture.completedFuture(null); } - PhysicsWorldSettings settings = resource.getWorldSettings(); + PhysicsWorldSettings settings = PhysicsWorlds.settings(physicsStore); settings.setEventCollectionMode(mode); - resource.setWorldSettings(settings); + PhysicsWorlds.putSettings(physicsStore, settings); ctx.sender().sendMessage(Message.raw("Impulse event collection set to " + mode.getSerializedName())); return CompletableFuture.completedFuture(null); diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/MaxStepDtSettingCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/MaxStepDtSettingCommand.java index 0c286be8..e4315aff 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/MaxStepDtSettingCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/MaxStepDtSettingCommand.java @@ -10,11 +10,14 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsWorlds; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; +// NOTE: probably move this to just an optional target in stepmode public class MaxStepDtSettingCommand extends AbstractAsyncPlayerCommand { private final OptionalArg dtArg = this.withOptionalArg( @@ -33,10 +36,10 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); + Store physicsStore = PhysicsThreading.store(world); if (!dtArg.provided(ctx)) { ctx.sender().sendMessage(Message.raw("Impulse max step dt: " - + resource.getWorldSettings().getMaxStepDt() + " (used by adaptive step modes)")); + + PhysicsWorlds.settings(physicsStore).getMaxStepDt())); return CompletableFuture.completedFuture(null); } @@ -46,9 +49,9 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, return CompletableFuture.completedFuture(null); } - PhysicsWorldSettings settings = resource.getWorldSettings(); + PhysicsWorldSettings settings = PhysicsWorlds.settings(physicsStore); settings.setMaxStepDt(maxStepDt); - resource.setWorldSettings(settings); + PhysicsWorlds.putSettings(physicsStore, settings); ctx.sender().sendMessage(Message.raw("Impulse max step dt set to " + maxStepDt)); return CompletableFuture.completedFuture(null); } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/SettingsCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/SettingsCommand.java index 6c6b4a6a..4a7e93e1 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/SettingsCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/SettingsCommand.java @@ -10,10 +10,9 @@ public SettingsCommand() { super("settings", "Impulse runtime settings commands"); addSubCommand(new SimulationSettingsCommand()); addSubCommand(new SolverSettingsCommand()); - addSubCommand(new VisualSettingsCommand()); } - public void addContribution(@Nonnull AbstractCommand command) { + public void registerSettingsCommand(@Nonnull AbstractCommand command) { addSubCommand(command); } } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/SimulationStepsSettingCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/SimulationStepsSettingCommand.java index 10adadcb..4c914364 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/SimulationStepsSettingCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/SimulationStepsSettingCommand.java @@ -10,7 +10,9 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsWorlds; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; import java.util.concurrent.CompletableFuture; @@ -34,8 +36,8 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); - PhysicsWorldSettings settings = resource.getWorldSettings(); + Store physicsStore = PhysicsThreading.store(world); + PhysicsWorldSettings settings = PhysicsWorlds.settings(physicsStore); PhysicsStepMode stepMode = settings.getStepMode(); if (!stepsArg.provided(ctx)) { ctx.sender().sendMessage(Message.raw("Impulse simulation steps: " @@ -54,7 +56,7 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, } settings.setSimulationSteps(steps); - resource.setWorldSettings(settings); + PhysicsWorlds.putSettings(physicsStore, settings); ctx.sender().sendMessage(Message.raw("Impulse simulation steps set to " + steps + " (" + stepMode.describeSimulationSteps() + " in " + stepMode.getSerializedName() + " mode)")); diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/SolverSettingsCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/SolverSettingsCommand.java index 3a265c3c..ebaf2714 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/SolverSettingsCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/SolverSettingsCommand.java @@ -1,22 +1,26 @@ package dev.hytalemodding.impulse.core.internal.commands.settings; +import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.command.system.CommandContext; import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; -import com.hypixel.hytale.server.core.command.system.basecommands.AbstractWorldCommand; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncWorldCommand; import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.commands.SpaceSelection; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.query.SolverCapabilityQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.SolverCapabilitySummary; +import dev.hytalemodding.impulse.core.internal.commands.space.SpaceSelection; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsDiagnostics; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsAsync; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsSpaces; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSolverSettings; +import dev.hytalemodding.impulse.core.plugin.physics.SolverCapabilitySummary; +import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; -public class SolverSettingsCommand extends AbstractWorldCommand { +public class SolverSettingsCommand extends AbstractAsyncWorldCommand { private final OptionalArg solverIterationsArg = this.withOptionalArg( "solverIterations", @@ -47,21 +51,34 @@ public SolverSettingsCommand() { super("solver", "Get or set solver tuning for a physics space", true); } + @Nonnull @Override - protected void execute(@Nonnull CommandContext ctx, - @Nonnull World world, - @Nonnull Store store) { - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); - SpaceId spaceId = SpaceSelection.resolve(ctx, world, resource, spaceArg); - if (spaceId == null) { - return; + protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, + @Nonnull World world) { + Store physicsStore = PhysicsThreading.store(world); + SpaceSelection.SelectedSpace selectedSpace = SpaceSelection.resolveStoreSpace(ctx, + world, + spaceArg); + if (selectedSpace == null) { + return CompletableFuture.completedFuture(null); } - SolverCapabilitySummary summary = resource.query(new SolverCapabilityQuery(spaceId)) - .completion() - .toCompletableFuture() - .join(); + SpaceId spaceId = selectedSpace.spaceId(); + return PhysicsAsync.acceptOnWorldThread(world, + PhysicsDiagnostics.solverCapabilityAsync(world, selectedSpace.spaceRef()), + summary -> applySettings(ctx, physicsStore, selectedSpace.spaceRef(), spaceId, summary)); + } - PhysicsSpaceSettings settings = new PhysicsSpaceSettings(resource.getSpaceSettings(spaceId)); + private void applySettings(@Nonnull CommandContext ctx, + @Nonnull Store physicsStore, + @Nonnull Ref spaceRef, + @Nonnull SpaceId spaceId, + @Nonnull SolverCapabilitySummary summary) { + PhysicsSolverSettings settings = PhysicsSpaces.solverSettings(physicsStore, spaceRef); + if (settings == null) { + ctx.sender().sendMessage(Message.raw("Physics space id=" + spaceId.value() + + " no longer exists.")); + return; + } if (!anyArgProvided(ctx)) { sendSummary(ctx, spaceId, summary, settings); return; @@ -69,19 +86,19 @@ protected void execute(@Nonnull CommandContext ctx, int solverIterations = solverIterationsArg.provided(ctx) ? solverIterationsArg.get(ctx) - : settings.getSolverSettings().getSolverIterations(); + : settings.getSolverIterations(); int stabilizationIterations = stabilizationIterationsArg.provided(ctx) ? stabilizationIterationsArg.get(ctx) - : settings.getSolverSettings().getStabilizationIterations(); + : settings.getStabilizationIterations(); float sleepLinearThreshold = sleepLinearThresholdArg.provided(ctx) ? sleepLinearThresholdArg.get(ctx) - : settings.getSolverSettings().getDynamicSleepLinearThreshold(); + : settings.getDynamicSleepLinearThreshold(); float sleepAngularThreshold = sleepAngularThresholdArg.provided(ctx) ? sleepAngularThresholdArg.get(ctx) - : settings.getSolverSettings().getDynamicSleepAngularThreshold(); + : settings.getDynamicSleepAngularThreshold(); float sleepTime = sleepTimeArg.provided(ctx) ? sleepTimeArg.get(ctx) - : settings.getSolverSettings().getDynamicSleepTimeUntilSleep(); + : settings.getDynamicSleepTimeUntilSleep(); if (solverIterations < 1 || stabilizationIterations < 0 @@ -96,10 +113,10 @@ protected void execute(@Nonnull CommandContext ctx, return; } - settings.getSolverSettings().setSolverIterations(solverIterations); - settings.getSolverSettings().setStabilizationIterations(stabilizationIterations); - settings.getSolverSettings().setDynamicSleepTuning(sleepLinearThreshold, sleepAngularThreshold, sleepTime); - resource.setSpaceSettings(spaceId, settings); + settings.setSolverIterations(solverIterations); + settings.setStabilizationIterations(stabilizationIterations); + settings.setDynamicSleepTuning(sleepLinearThreshold, sleepAngularThreshold, sleepTime); + PhysicsSpaces.putSolverSettings(physicsStore, spaceRef, settings); sendSummary(ctx, spaceId, summary, settings); } @@ -114,17 +131,17 @@ private boolean anyArgProvided(@Nonnull CommandContext ctx) { private static void sendSummary(@Nonnull CommandContext ctx, @Nonnull SpaceId spaceId, @Nonnull SolverCapabilitySummary summary, - @Nonnull PhysicsSpaceSettings settings) { + @Nonnull PhysicsSolverSettings settings) { ctx.sender().sendMessage(Message.raw("Impulse solver settings for space " + spaceId.value() + " backend=" + summary.backendId() + " solverApplied=" + summary.solverTuningSupported() + " sleepApplied=" + summary.activationTuningSupported() - + ": solverIterations=" + settings.getSolverSettings().getSolverIterations() - + " stabilizationIterations=" + settings.getSolverSettings().getStabilizationIterations() - + " sleepLinearThreshold=" + settings.getSolverSettings().getDynamicSleepLinearThreshold() - + " sleepAngularThreshold=" + settings.getSolverSettings().getDynamicSleepAngularThreshold() - + " sleepTime=" + settings.getSolverSettings().getDynamicSleepTimeUntilSleep())); + + ": solverIterations=" + settings.getSolverIterations() + + " stabilizationIterations=" + settings.getStabilizationIterations() + + " sleepLinearThreshold=" + settings.getDynamicSleepLinearThreshold() + + " sleepAngularThreshold=" + settings.getDynamicSleepAngularThreshold() + + " sleepTime=" + settings.getDynamicSleepTimeUntilSleep())); } } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/StepModeSettingCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/StepModeSettingCommand.java index 0bca6d46..e3af4617 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/StepModeSettingCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/StepModeSettingCommand.java @@ -10,11 +10,14 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsDiagnostics; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsAsync; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsWorlds; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.SpaceSummary; -import dev.hytalemodding.impulse.core.plugin.simulation.query.UnsupportedCcdSpacesQuery; +import dev.hytalemodding.impulse.core.plugin.physics.SpaceSummary; import java.util.List; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; @@ -37,10 +40,10 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); + Store physicsStore = PhysicsThreading.store(world); if (!modeArg.provided(ctx)) { ctx.sender().sendMessage(Message.raw("Impulse step mode: " - + resource.getWorldSettings().getStepMode().getSerializedName())); + + PhysicsWorlds.settings(physicsStore).getStepMode().getSerializedName())); return CompletableFuture.completedFuture(null); } @@ -54,31 +57,38 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, } if (stepMode == PhysicsStepMode.CCD) { - List unsupportedSpaces = unsupportedCcdSpaces(resource); - if (!unsupportedSpaces.isEmpty()) { - ctx.sender().sendMessage(Message.raw("CCD mode is not available for: " - + String.join(", ", unsupportedSpaces))); - return CompletableFuture.completedFuture(null); - } + return PhysicsAsync.acceptOnWorldThread(world, + PhysicsDiagnostics.unsupportedCcdSpacesAsync(world), + summaries -> applyStepModeIfSupported(ctx, physicsStore, stepMode, summaries)); } - PhysicsWorldSettings settings = resource.getWorldSettings(); - settings.setStepMode(stepMode); - resource.setWorldSettings(settings); - ctx.sender().sendMessage(Message.raw("Impulse step mode set to " - + stepMode.getSerializedName())); + applyStepMode(ctx, physicsStore, stepMode); return CompletableFuture.completedFuture(null); } - @Nonnull - private static List unsupportedCcdSpaces(@Nonnull PhysicsWorldResource resource) { - return resource.query(new UnsupportedCcdSpacesQuery()) - .completion() - .toCompletableFuture() - .join() - .stream() + private static void applyStepModeIfSupported(@Nonnull CommandContext ctx, + @Nonnull Store physicsStore, + @Nonnull PhysicsStepMode stepMode, + @Nonnull List unsupportedSummaries) { + List unsupportedSpaces = unsupportedSummaries.stream() .map(StepModeSettingCommand::formatSpace) .toList(); + if (!unsupportedSpaces.isEmpty()) { + ctx.sender().sendMessage(Message.raw("CCD mode is not available for: " + + String.join(", ", unsupportedSpaces))); + return; + } + applyStepMode(ctx, physicsStore, stepMode); + } + + private static void applyStepMode(@Nonnull CommandContext ctx, + @Nonnull Store physicsStore, + @Nonnull PhysicsStepMode stepMode) { + PhysicsWorldSettings settings = PhysicsWorlds.settings(physicsStore); + settings.setStepMode(stepMode); + PhysicsWorlds.putSettings(physicsStore, settings); + ctx.sender().sendMessage(Message.raw("Impulse step mode set to " + + stepMode.getSerializedName())); } @Nonnull diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/StepSchedulingSettingCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/StepSchedulingSettingCommand.java index a7205eaf..9101550f 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/StepSchedulingSettingCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/StepSchedulingSettingCommand.java @@ -10,7 +10,9 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsWorlds; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepSchedulingMode; import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; import java.util.concurrent.CompletableFuture; @@ -20,11 +22,11 @@ public class StepSchedulingSettingCommand extends AbstractAsyncPlayerCommand { private final OptionalArg modeArg = this.withOptionalArg( "mode", - "Step scheduling mode: drop_pending_dt or accumulate_pending_dt", + "Step scheduling mode: drop_pending_dt keeps cadence steady; accumulate_pending_dt catches up", ArgTypes.STRING); public StepSchedulingSettingCommand() { - super("scheduling", "Get or set how pending owner dt is handled"); + super("scheduling", "Get or set how pending store tick dt is handled"); } @Nonnull @@ -34,9 +36,10 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); + Store physicsStore = PhysicsThreading.store(world); if (!modeArg.provided(ctx)) { - PhysicsStepSchedulingMode mode = resource.getWorldSettings().getStepSchedulingMode(); + PhysicsStepSchedulingMode mode = PhysicsWorlds.settings(physicsStore) + .getStepSchedulingMode(); ctx.sender().sendMessage(Message.raw("Impulse step scheduling: " + mode.getSerializedName() + " (" + mode.describePendingStepBehavior() + ")")); return CompletableFuture.completedFuture(null); @@ -47,13 +50,13 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, mode = PhysicsStepSchedulingMode.parse(modeArg.get(ctx)); } catch (IllegalArgumentException exception) { ctx.sender().sendMessage(Message.raw("Unknown step scheduling mode. Use one of: " - + "drop_pending_dt, accumulate_pending_dt.")); + + "drop_pending_dt (steady/no catch-up), accumulate_pending_dt (catch-up).")); return CompletableFuture.completedFuture(null); } - PhysicsWorldSettings settings = resource.getWorldSettings(); + PhysicsWorldSettings settings = PhysicsWorlds.settings(physicsStore); settings.setStepSchedulingMode(mode); - resource.setWorldSettings(settings); + PhysicsWorlds.putSettings(physicsStore, settings); ctx.sender().sendMessage(Message.raw("Impulse step scheduling set to " + mode.getSerializedName() + " (" + mode.describePendingStepBehavior() + ")")); return CompletableFuture.completedFuture(null); diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceCommand.java new file mode 100644 index 00000000..acedfd9d --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceCommand.java @@ -0,0 +1,85 @@ +package dev.hytalemodding.impulse.core.internal.commands.space; + +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.ImpulseBackendRegistry; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; +import dev.hytalemodding.impulse.core.ImpulsePlugin; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CompletionException; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class SpaceCommand extends AbstractCommandCollection { + + public SpaceCommand() { + super("space", "Explicit physics space lifecycle commands"); + addSubCommand(new SpaceCreateCommand()); + addSubCommand(new SpaceListCommand()); + addSubCommand(new SpaceDeleteCommand()); + } + + @Nullable + static BackendId parseBackendId(@Nonnull CommandContext context, + @Nonnull OptionalArg backendArg, + boolean useDefaultWhenMissing) { + if (!backendArg.provided(context)) { + if (!useDefaultWhenMissing) { + return null; + } + + BackendId defaultBackendId = ImpulsePlugin.get().getDefaultBackendId(); + if (defaultBackendId != null) { + return defaultBackendId; + } + + context.sendMessage(Message.raw("Missing backend id. Multiple backends are installed; " + + "use --backend=. Available backends: " + availableBackendIds())); + return null; + } + + String rawBackendId = backendArg.get(context).trim(); + try { + return new BackendId(rawBackendId); + } catch (RuntimeException exception) { + context.sendMessage(Message.raw("Invalid backend id: " + rawBackendId)); + return null; + } + } + + @Nullable + static PhysicsChunkCollisionMode parsePhysicsChunkMode(@Nonnull String value) { + return switch (value.toLowerCase(Locale.ROOT)) { + case "none", "off", "disabled" -> PhysicsChunkCollisionMode.NONE; + case "manual" -> PhysicsChunkCollisionMode.MANUAL; + case "streaming", "stream", "on", "enabled" -> PhysicsChunkCollisionMode.STREAMING; + default -> null; + }; + } + + @Nonnull + private static String availableBackendIds() { + List backendIds = new ArrayList<>(); + for (PhysicsBackendRuntimeProvider provider : ImpulseBackendRegistry.getRuntimeProviders()) { + backendIds.add(provider.getId().value()); + } + backendIds.sort(String::compareTo); + return backendIds.isEmpty() ? "" : String.join(", ", backendIds); + } + + + @Nonnull + static Throwable unwrap(@Nonnull Throwable failure) { + if (failure instanceof CompletionException completionException + && completionException.getCause() != null) { + return completionException.getCause(); + } + return failure; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceCreateCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceCreateCommand.java new file mode 100644 index 00000000..c680ff70 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceCreateCommand.java @@ -0,0 +1,75 @@ +package dev.hytalemodding.impulse.core.internal.commands.space; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; +import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractWorldCommand; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.ImpulseBackendRegistry; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsChunkCollisionSettings; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsSpaces; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import javax.annotation.Nonnull; +import java.util.Locale; + +public class SpaceCreateCommand extends AbstractWorldCommand { + + private final OptionalArg backendArg = withOptionalArg( + "backend", + "Backend id, for example impulse:rapier", + ArgTypes.STRING); + private final OptionalArg physicsChunkArg = withOptionalArg( + "physicsChunk", + "PhysicsChunk collision mode: none, manual, or streaming", + ArgTypes.STRING); + + SpaceCreateCommand() { + super("create", "Create an explicit physics space", false); + } + + @Override + protected void execute(@Nonnull CommandContext context, + @Nonnull World world, + @Nonnull Store store) { + BackendId backendId = SpaceCommand.parseBackendId(context, backendArg, true); + if (backendId == null) { + return; + } + + PhysicsChunkCollisionMode physicsChunkMode = physicsChunkArg.provided(context) + ? SpaceCommand.parsePhysicsChunkMode(physicsChunkArg.get(context)) + : PhysicsChunkCollisionMode.STREAMING; + if (physicsChunkMode == null) { + context.sendMessage( + Message.raw("physicsChunk must be none, manual, or streaming.")); + return; + } + + Store physicsStore = PhysicsThreading.store(world); + try { + ImpulseBackendRegistry.getRuntimeProvider(backendId); + SpaceId spaceId = PhysicsSpaces.create(physicsStore, backendId); + PhysicsChunkCollisionSettings chunkCollisionSettings = + new PhysicsChunkCollisionSettings(); + chunkCollisionSettings.setMode(physicsChunkMode); + PhysicsSpaces.putChunkCollisionSettings(physicsStore, + spaceId, + chunkCollisionSettings); + context.sendMessage(Message.raw("Created physics space id=" + + spaceId.value() + + " backend=" + backendId.value() + + " physicsChunk=" + physicsChunkMode.name().toLowerCase(Locale.ROOT) + + ".")); + } catch (RuntimeException exception) { + context.sendMessage(Message.raw("Failed to create physics space: " + + exception.getMessage())); + } + } +} \ No newline at end of file diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceDeleteCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceDeleteCommand.java new file mode 100644 index 00000000..22feebbf --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceDeleteCommand.java @@ -0,0 +1,94 @@ +package dev.hytalemodding.impulse.core.internal.commands.space; + +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; +import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncWorldCommand; +import com.hypixel.hytale.server.core.universe.world.World; +import dev.hytalemodding.impulse.core.internal.commands.space.SpaceDeleteSupport.DeleteResult; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.concurrent.CompletableFuture; + +public class SpaceDeleteCommand extends AbstractAsyncWorldCommand { + + private final OptionalArg spaceArg = withOptionalArg( + "space", + "Space id to delete", + ArgTypes.INTEGER); + + SpaceDeleteCommand() { + super("delete", "Delete a physics space and its runtime backend state", true); + } + + @Nonnull + @Override + protected CompletableFuture executeAsync(@Nonnull CommandContext context, + @Nonnull World world) { + if (!spaceArg.provided(context)) { + context.sendMessage(Message.raw("Missing space id. Example:" + + " /impulse space delete --space=1 --confirm")); + return CompletableFuture.completedFuture(null); + } + + int rawSpaceId = spaceArg.get(context); + return PhysicsThreading.callWhenBackendIdleOnWorldThread(world, + "delete PhysicsStore space", + physicsStore -> SpaceDeleteSupport.deleteOnWorldThread(world, + physicsStore, + rawSpaceId)) + .handle((result, failure) -> { + sendDeleteResult(context, world, result, failure); + return (Void) null; + }) + .toCompletableFuture(); + } + + private static void sendDeleteResult(@Nonnull CommandContext context, + @Nonnull World world, + @Nullable DeleteResult result, + @Nullable Throwable failure) { + Runnable sender = () -> { + if (failure != null) { + Throwable cause = SpaceCommand.unwrap(failure); + String message = cause.getMessage() != null + ? cause.getMessage() + : cause.toString(); + context.sendMessage(Message.raw("Failed to delete physics space: " + + message)); + return; + } + if (result == null) { + context.sendMessage(Message.raw("Failed to delete physics space.")); + return; + } + switch (result.outcome()) { + case INVALID -> context.sendMessage(Message.raw( + "Space id must be a positive integer.")); + case MISSING -> context.sendMessage(Message.raw("No physics space id=" + + result.rawSpaceId() + + " exists in world " + world.getName() + ".")); + case UNBOUND -> context.sendMessage(Message.raw("PhysicsStore space id=" + + result.rawSpaceId() + + " is not bound in world " + world.getName() + ".")); + case NOT_EMPTY -> context.sendMessage(Message.raw("Physics space id=" + + result.rawSpaceId() + + " is not empty (" + result.registeredBodies() + + " registered bodies, " + result.backendBodies() + + " backend bodies, " + result.joints() + " joints)." + + " Use /impulse clean for populated worlds, then delete empty spaces.")); + case DELETED -> context.sendMessage(Message.raw("Deleted physics space id=" + + result.rawSpaceId() + + " with " + result.backendBodies() + + " backend bodies and " + result.joints() + " joints.")); + } + }; + if (world.isInThread()) { + sender.run(); + return; + } + world.execute(sender); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceDeleteSupport.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceDeleteSupport.java new file mode 100644 index 00000000..e24dc750 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceDeleteSupport.java @@ -0,0 +1,151 @@ +package dev.hytalemodding.impulse.core.internal.commands.space; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollision; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsDiagnostics; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsSpaces; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import dev.hytalemodding.impulse.core.plugin.physics.SpaceSummary; +import java.util.List; +import java.util.UUID; +import javax.annotation.Nonnull; + +final class SpaceDeleteSupport { + + private SpaceDeleteSupport() { + } + + @Nonnull + static DeleteResult deleteOnWorldThread( + @Nonnull World world, + @Nonnull Store physicsStore, + int rawSpaceId) { + PhysicsThreading.requireWorldThread(physicsStore, "delete PhysicsStore space"); + if (rawSpaceId <= 0) { + return DeleteResult.invalid(rawSpaceId); + } + + PhysicsSpaceCompatibilityIndexResource compatibility = physicsStore.getResource( + PhysicsSpaceCompatibilityIndexResource.getResourceType()); + SpaceId spaceId = SpaceSelection.specifiedSpaceId(compatibility, rawSpaceId); + if (spaceId == null) { + return DeleteResult.missing(rawSpaceId); + } + + UUID spaceUuid = compatibility.getSpaceUuid(spaceId); + Ref spaceRef = spaceUuid != null + ? physicsStore.getExternalData().getRefFromUUID(spaceUuid) + : null; + if (spaceRef == null || spaceRef.getStore() != physicsStore || !spaceRef.isValid()) { + return DeleteResult.unbound(rawSpaceId); + } + + /* + * Streaming PhysicsChunk collision is represented as normal PhysicsStore body rows with + * copied snapshots. Those rows belong to the space/cache lifecycle and are removed with + * the space/cache. Non-PhysicsChunk body rows are gameplay/runtime resources addressed by + * durable body UUID or live PhysicsStore entity ref, so they still require explicit + * clean/destroy before deleting the space. + */ + int registeredBodies = registeredBodiesExcludingChunkCollision(physicsStore, + compatibility, + spaceId); + List summaries = PhysicsDiagnostics.spaceSummaries(physicsStore, + spaceRef); + SpaceCounts counts = countSpaceContents(summaries, spaceId); + int backendBodies = counts.bodies(); + int joints = counts.joints(); + if (registeredBodies > 0 || joints > 0) { + return DeleteResult.notEmpty(rawSpaceId, + registeredBodies, + backendBodies, + joints); + } + + PhysicsChunkCollision.clearSpace(world, physicsStore, spaceRef); + PhysicsSpaces.removeWithContents(physicsStore, spaceId); + return DeleteResult.deleted(rawSpaceId, backendBodies, joints); + } + + private static int registeredBodiesExcludingChunkCollision( + @Nonnull Store store, + @Nonnull PhysicsSpaceCompatibilityIndexResource compatibility, + @Nonnull SpaceId spaceId) { + int count = 0; + for (PhysicsBodySnapshot snapshot : PhysicsBodies.snapshotFrame(store).bodies()) { + if (!spaceId.equals(compatibility.getSpaceId(snapshot.spaceUuid())) + || PhysicsChunkCollision.isChunkCollisionBody(store, snapshot.bodyUuid())) { + continue; + } + count++; + } + return count; + } + + @Nonnull + private static SpaceCounts countSpaceContents(@Nonnull List summaries, + @Nonnull SpaceId spaceId) { + return summaries.stream() + .filter(summary -> summary.spaceId().equals(spaceId)) + .findFirst() + .map(summary -> new SpaceCounts(summary.bodyCount(), summary.jointCount())) + .orElseGet(() -> new SpaceCounts(0, 0)); + } + + enum DeleteOutcome { + INVALID, + MISSING, + UNBOUND, + NOT_EMPTY, + DELETED + } + + record DeleteResult(@Nonnull DeleteOutcome outcome, + int rawSpaceId, + int registeredBodies, + int backendBodies, + int joints) { + + @Nonnull + private static DeleteResult invalid(int rawSpaceId) { + return new DeleteResult(DeleteOutcome.INVALID, rawSpaceId, 0, 0, 0); + } + + @Nonnull + private static DeleteResult missing(int rawSpaceId) { + return new DeleteResult(DeleteOutcome.MISSING, rawSpaceId, 0, 0, 0); + } + + @Nonnull + private static DeleteResult unbound(int rawSpaceId) { + return new DeleteResult(DeleteOutcome.UNBOUND, rawSpaceId, 0, 0, 0); + } + + @Nonnull + private static DeleteResult notEmpty(int rawSpaceId, + int registeredBodies, + int backendBodies, + int joints) { + return new DeleteResult(DeleteOutcome.NOT_EMPTY, + rawSpaceId, + registeredBodies, + backendBodies, + joints); + } + + @Nonnull + private static DeleteResult deleted(int rawSpaceId, int backendBodies, int joints) { + return new DeleteResult(DeleteOutcome.DELETED, rawSpaceId, 0, backendBodies, joints); + } + } + + private record SpaceCounts(int bodies, int joints) { + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceListCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceListCommand.java new file mode 100644 index 00000000..4c6ab34d --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceListCommand.java @@ -0,0 +1,82 @@ +package dev.hytalemodding.impulse.core.internal.commands.space; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncWorldCommand; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsChunkCollisionSettings; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsAsync; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsDiagnostics; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsSpaces; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.physics.SpaceSummary; +import javax.annotation.Nonnull; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.CompletableFuture; + +public class SpaceListCommand extends AbstractAsyncWorldCommand { + + SpaceListCommand() { + super("list", "List physics spaces in the target world", false); + } + + @Nonnull + @Override + protected CompletableFuture executeAsync(@Nonnull CommandContext context, + @Nonnull World world) { + Store physicsStore = PhysicsThreading.store(world); + return PhysicsAsync.acceptOnWorldThread(world, + PhysicsDiagnostics.spaceSummariesAsync(world), + summaries -> sendSpaces(context, world, physicsStore, summaries)); + } + + private static void sendSpaces(@Nonnull CommandContext context, + @Nonnull World world, + @Nonnull Store physicsStore, + @Nonnull List summaries) { + List spaces = summaries.stream() + .map(summary -> { + PhysicsChunkCollisionSettings settings = PhysicsSpaces.chunkCollisionSettings( + physicsStore, + summary.spaceId()); + PhysicsChunkCollisionMode physicsChunkMode = settings != null + ? settings.getMode() + : PhysicsChunkCollisionMode.NONE; + return new SpaceListEntry(summary.spaceId(), + summary.backendId().value(), + summary.bodyCount(), + summary.jointCount(), + physicsChunkMode); + }) + .sorted(Comparator.comparingInt(entry -> entry.spaceId().value())) + .toList(); + + context.sendMessage(Message.raw("Physics spaces in world " + world.getName() + ":")); + if (spaces.isEmpty()) { + context.sendMessage(Message.raw("- ")); + return; + } + + for (SpaceListEntry space : spaces) { + context.sendMessage(Message.raw("- id=" + space.spaceId().value() + + " backend=" + space.backendId() + + " bodies=" + space.bodies() + + " joints=" + space.joints() + + " physicsChunk=" + + space.physicsChunkMode().name().toLowerCase(Locale.ROOT))); + } + } + + private record SpaceListEntry(@Nonnull SpaceId spaceId, + @Nonnull String backendId, + int bodies, + int joints, + @Nonnull PhysicsChunkCollisionMode physicsChunkMode) { + } +} \ No newline at end of file diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceSelection.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceSelection.java new file mode 100644 index 00000000..e6b3e009 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceSelection.java @@ -0,0 +1,123 @@ +package dev.hytalemodding.impulse.core.internal.commands.space; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import java.util.Comparator; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public final class SpaceSelection { + + private SpaceSelection() { + } + + @Nullable + public static SpaceId resolve(@Nonnull CommandContext context, + @Nonnull World world, + @Nonnull OptionalArg spaceArg) { + PhysicsSpaceCompatibilityIndexResource compatibility = compatibility(store(world)); + return resolveSpaceId(context, world, spaceArg, compatibility); + } + + @Nullable + public static SelectedSpace resolveStoreSpace(@Nonnull CommandContext context, + @Nonnull World world, + @Nonnull OptionalArg spaceArg) { + Store store = store(world); + PhysicsSpaceCompatibilityIndexResource compatibility = compatibility(store); + SpaceId spaceId = resolveSpaceId(context, world, spaceArg, compatibility); + if (spaceId == null) { + return null; + } + + UUID spaceUuid = compatibility.getSpaceUuid(spaceId); + Ref spaceRef = spaceUuid != null + ? store.getExternalData().getRefFromUUID(spaceUuid) + : null; + if (spaceRef == null || spaceRef.getStore() != store || !spaceRef.isValid()) { + context.sendMessage(Message.raw("PhysicsStore space id=" + spaceId.value() + + " is not bound in world " + world.getName() + ".")); + return null; + } + return new SelectedSpace(spaceId, spaceRef); + } + + @Nullable + private static SpaceId resolveSpaceId(@Nonnull CommandContext context, + @Nonnull World world, + @Nonnull OptionalArg spaceArg, + @Nonnull PhysicsSpaceCompatibilityIndexResource compatibility) { + if (spaceArg.provided(context)) { + int rawSpaceId = spaceArg.get(context); + if (rawSpaceId <= 0) { + context.sendMessage(Message.raw("Space id must be a positive integer.")); + return null; + } + SpaceId spaceId = specifiedSpaceId(compatibility, rawSpaceId); + if (spaceId == null) { + context.sendMessage(Message.raw("No physics space id=" + rawSpaceId + + " exists in world " + world.getName() + ".")); + return null; + } + return spaceId; + } + + SpaceId firstSpaceId = firstRegisteredSpaceId(compatibility); + if (firstSpaceId == null) { + context.sendMessage(Message.raw("No physics space exists. Run " + + "`/impulse space create --backend=` before targeting space settings.")); + } + return firstSpaceId; + } + + @Nullable + static SpaceId specifiedSpaceId(@Nonnull PhysicsSpaceCompatibilityIndexResource compatibility, + int rawSpaceId) { + if (rawSpaceId <= 0) { + return null; + } + SpaceId spaceId = new SpaceId(rawSpaceId); + return compatibility.hasSpace(spaceId) ? spaceId : null; + } + + @Nullable + static SpaceId firstRegisteredSpaceId( + @Nonnull PhysicsSpaceCompatibilityIndexResource compatibility) { + return compatibility.spaceIds() + .stream() + .min(Comparator.comparingInt(SpaceId::value)) + .orElse(null); + } + + @Nonnull + private static Store store(@Nonnull World world) { + Store store = PhysicsThreading.store(world); + PhysicsThreading.requireWorldThread(store, "select a PhysicsStore space"); + return store; + } + + @Nonnull + private static PhysicsSpaceCompatibilityIndexResource compatibility( + @Nonnull Store store) { + return store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()); + } + + public record SelectedSpace(@Nonnull SpaceId spaceId, + @Nonnull Ref spaceRef) { + + public SelectedSpace { + Objects.requireNonNull(spaceId, "spaceId"); + Objects.requireNonNull(spaceRef, "spaceRef"); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ControlSubPluginCrucibleSupport.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ControlSubPluginCrucibleSupport.java deleted file mode 100644 index 3ea76160..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ControlSubPluginCrucibleSupport.java +++ /dev/null @@ -1,165 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.crucible; - -import com.hypixel.hytale.common.plugin.PluginIdentifier; -import com.hypixel.hytale.component.AddReason; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Holder; -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.entity.entities.BlockEntity; -import com.hypixel.hytale.server.core.modules.entity.DespawnComponent; -import com.hypixel.hytale.server.core.modules.time.TimeResource; -import com.hypixel.hytale.server.core.plugin.PluginBase; -import com.hypixel.hytale.server.core.plugin.PluginManager; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.modules.control.ControlLifecycle; -import dev.hytalemodding.impulse.core.internal.modules.control.components.PhysicsControlSessionComponent; -import dev.hytalemodding.impulse.core.plugin.modules.control.ImpulseControllableComponent; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.TimeUnit; -import javax.annotation.Nonnull; -import org.joml.Vector3d; - -/** - * Runtime-only helpers for exercising the control subplugin through Hytale. - */ -final class ControlSubPluginCrucibleSupport { - - private static final PluginIdentifier PLUGIN_ID = - new PluginIdentifier("HytaleModding", "ImpulseControl"); - private static final String DEFAULT_BLOCK_TYPE = "Rock_Stone"; - private static final long SMOKE_TIMEOUT_SECONDS = 30L; - private static final ComponentType DESPAWN_TYPE = - DespawnComponent.getComponentType(); - - private ControlSubPluginCrucibleSupport() { - } - - @Nonnull - static CompletionStage loadUnloadReloadSmokeAsync( - @Nonnull CrucibleContext context) { - return CompletableFuture.supplyAsync(() -> loadUnloadReloadSmoke(context)) - .orTimeout(SMOKE_TIMEOUT_SECONDS, TimeUnit.SECONDS) - .exceptionally(failure -> CrucibleTestCase.TestOutcome.fail( - "Control subplugin lifecycle smoke failed: " + failure.getMessage())); - } - - private static CrucibleTestCase.TestOutcome loadUnloadReloadSmoke( - @Nonnull CrucibleContext context) { - try { - PluginManager pluginManager = PluginManager.get(); - if (!pluginManager.getAvailablePlugins().containsKey(PLUGIN_ID) - && pluginManager.getPlugin(PLUGIN_ID) == null) { - return CrucibleTestCase.TestOutcome.fail( - "Control subplugin is not available: " + PLUGIN_ID); - } - - if (!ensureLoaded(pluginManager)) { - return CrucibleTestCase.TestOutcome.fail( - "Control subplugin load did not enable the lifecycle"); - } - Ref controllableRef = addControllableEntity(context); - if (!controllableRef.isValid()) { - return CrucibleTestCase.TestOutcome.fail("Failed to add controllable test entity"); - } - if (!pluginManager.unload(PLUGIN_ID)) { - return CrucibleTestCase.TestOutcome.fail( - "Control subplugin unload returned false"); - } - if (pluginManager.getPlugin(PLUGIN_ID) != null) { - return CrucibleTestCase.TestOutcome.fail( - "Control subplugin remained loaded after unload"); - } - if (ControlLifecycle.isEnabled()) { - return CrucibleTestCase.TestOutcome.fail( - "Control subplugin unload did not disable the lifecycle"); - } - boolean loadResult = pluginManager.load(PLUGIN_ID); - PluginBase loadedPlugin = pluginManager.getPlugin(PLUGIN_ID); - if (!loadResult || !ControlLifecycle.isEnabled()) { - return CrucibleTestCase.TestOutcome.fail( - "Control subplugin reload load did not enable the lifecycle: " - + "loadResult=" + loadResult - + ", pluginState=" + stateOf(loadedPlugin) - + ", lifecycleEnabled=" + ControlLifecycle.isEnabled()); - } - boolean reloadResult = pluginManager.reload(PLUGIN_ID); - PluginBase reloadedPlugin = pluginManager.getPlugin(PLUGIN_ID); - if (!reloadResult || !ControlLifecycle.isEnabled()) { - return CrucibleTestCase.TestOutcome.fail( - "Control subplugin reload did not leave the lifecycle enabled: " - + "reloadResult=" + reloadResult - + ", pluginState=" + stateOf(reloadedPlugin) - + ", lifecycleEnabled=" + ControlLifecycle.isEnabled()); - } - return CrucibleTestCase.TestOutcome.pass(); - } catch (ReflectiveOperationException | RuntimeException exception) { - return CrucibleTestCase.TestOutcome.fail(exception.getMessage()); - } - } - - private static boolean ensureLoaded(@Nonnull PluginManager pluginManager) { - if (pluginManager.getPlugin(PLUGIN_ID) != null && ControlLifecycle.isEnabled()) { - return true; - } - return pluginManager.load(PLUGIN_ID) && ControlLifecycle.isEnabled(); - } - - @Nonnull - private static Ref addControllableEntity(@Nonnull CrucibleContext context) - throws ReflectiveOperationException { - World world = context.world(); - if (world.isInThread()) { - return addControllableEntityOnWorldThread(context, world); - } - - CompletableFuture> entity = new CompletableFuture<>(); - try { - world.execute(() -> { - try { - entity.complete(addControllableEntityOnWorldThread(context, world)); - } catch (Throwable throwable) { - entity.completeExceptionally(throwable); - } - }); - } catch (RuntimeException exception) { - entity.completeExceptionally(exception); - } - - try { - return entity.orTimeout(SMOKE_TIMEOUT_SECONDS, TimeUnit.SECONDS).join(); - } catch (CompletionException exception) { - Throwable cause = exception.getCause(); - if (cause instanceof ReflectiveOperationException reflectiveOperationException) { - throw reflectiveOperationException; - } - throw exception; - } - } - - @Nonnull - private static Ref addControllableEntityOnWorldThread( - @Nonnull CrucibleContext context, - @Nonnull World world) throws ReflectiveOperationException { - Store store = world.getEntityStore().getStore(); - TimeResource time = store.getResource(TimeResource.getResourceType()); - Holder holder = BlockEntity.assembleDefaultBlockEntity( - time, - DEFAULT_BLOCK_TYPE, - new Vector3d(context.wx(0), context.wy(8), context.wz(0))); - holder.removeComponent(DESPAWN_TYPE); - holder.addComponent(ImpulseControllableComponent.getComponentType(), - new ImpulseControllableComponent()); - holder.addComponent(PhysicsControlSessionComponent.getComponentType(), - new PhysicsControlSessionComponent()); - return store.addEntity(holder, AddReason.SPAWN); - } - - @Nonnull - private static String stateOf(PluginBase plugin) { - return plugin == null ? "missing" : plugin.getState().name(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleBackends.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleBackends.java deleted file mode 100644 index 313c2953..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleBackends.java +++ /dev/null @@ -1,71 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.crucible; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -final class CrucibleBackends { - - private static final String BACKEND_PROPERTY = "impulse.crucible.backend"; - private static final BackendId RAPIER_BACKEND_ID = new BackendId("impulse:rapier"); - - private CrucibleBackends() { - } - - @Nonnull - static BackendId requireBackendId() { - return selectBackendId(Impulse.getRuntimeProviders(), System.getProperty(BACKEND_PROPERTY)); - } - - @Nonnull - static BackendId selectBackendId(@Nonnull Collection providers, - @Nullable String configuredBackendId) { - List backendIds = new ArrayList<>(); - for (PhysicsBackendRuntimeProvider provider : providers) { - backendIds.add(provider.getId()); - } - backendIds.sort(Comparator.comparing(BackendId::value)); - - if (backendIds.isEmpty()) { - throw new IllegalStateException("No physics backend runtimes registered"); - } - - String configured = configuredBackendId == null ? "" : configuredBackendId.trim(); - if (!configured.isEmpty()) { - BackendId requestedBackendId = new BackendId(configured); - if (backendIds.contains(requestedBackendId)) { - return requestedBackendId; - } - throw new IllegalStateException("Configured Crucible physics backend " - + requestedBackendId + " is not registered. Available backend runtimes: " - + formatBackendIds(backendIds)); - } - - if (backendIds.contains(RAPIER_BACKEND_ID)) { - return RAPIER_BACKEND_ID; - } - - if (backendIds.size() == 1) { - return backendIds.getFirst(); - } - - throw new IllegalStateException("Multiple physics backends are registered and Rapier is " - + "not available. Set -D" + BACKEND_PROPERTY + "=. Available backends: " - + formatBackendIds(backendIds)); - } - - @Nonnull - private static String formatBackendIds(@Nonnull List backendIds) { - List values = new ArrayList<>(); - for (BackendId backendId : backendIds) { - values.add(backendId.value()); - } - return String.join(", ", values); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleBridge.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleBridge.java deleted file mode 100644 index f4380215..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleBridge.java +++ /dev/null @@ -1,197 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.crucible; - -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import dev.hytalemodding.impulse.core.internal.crucible.CrucibleTestCase.TestOutcome; - -/** - * Reflective adapter for Crucible's API. - * - *

No class in impulse-examples should statically reference Crucible API - * types because Crucible is an optional directory-loaded mod with its own - * plugin classloader.

- */ -final class CrucibleBridge { - - private final Class asyncTestSuiteClass; - private final Method registerSuite; - private final Method pass; - private final Method fail; - private final Method error; - - private CrucibleBridge( - Class asyncTestSuiteClass, - Method registerSuite, - Method pass, - Method fail, - Method error) { - - this.asyncTestSuiteClass = asyncTestSuiteClass; - this.registerSuite = registerSuite; - this.pass = pass; - this.fail = fail; - this.error = error; - } - - static CrucibleBridge create(ClassLoader loader) throws ReflectiveOperationException { - Class apiClass = Class.forName( - "ionforgelabs.crucible.api.CrucibleAPI", true, loader); - Class testSuiteClass = Class.forName( - "ionforgelabs.crucible.api.TestSuite", true, loader); - Class asyncTestSuiteClass = Class.forName( - "ionforgelabs.crucible.api.AsyncTestSuite", true, loader); - Class resultClass = Class.forName( - "ionforgelabs.crucible.api.TestResult", true, loader); - - return new CrucibleBridge( - asyncTestSuiteClass, - apiClass.getMethod("registerSuite", testSuiteClass), - resultClass.getMethod("pass", String.class, String.class), - resultClass.getMethod("fail", String.class, String.class, String.class), - resultClass.getMethod("error", String.class, String.class, Exception.class)); - } - - void registerSuite(ClassLoader loader, CrucibleSuite suite) - throws ReflectiveOperationException { - - Object proxy = Proxy.newProxyInstance(loader, - new Class[] {asyncTestSuiteClass}, - new SuiteInvocationHandler(this, suite)); - registerSuite.invoke(null, proxy); - } - - private Object pass(String suite, String test) throws ReflectiveOperationException { - return pass.invoke(null, suite, test); - } - - private Object fail(String suite, String test, String message) - throws ReflectiveOperationException { - - return fail.invoke(null, suite, test, message); - } - - private Object error(String suite, String test, Exception exception) - throws ReflectiveOperationException { - - return error.invoke(null, suite, test, exception); - } - - private record SuiteInvocationHandler(CrucibleBridge bridge, CrucibleSuite suite) implements - InvocationHandler { - - @Override - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - return switch (method.getName()) { - case "id", "toString" -> suite.id(); - case "name" -> suite.name(); - case "description" -> suite.description(); - case "tags" -> suite.tags(); - case "run" -> runTests(context(args)); - case "runAsync" -> runTestsAsync(context(args)); - case "hashCode" -> suite.id().hashCode(); - case "equals" -> args != null && args.length > 0 && proxy == args[0]; - default -> throw new UnsupportedOperationException( - "Unsupported Crucible TestSuite method: " + method.getName()); - }; - } - - private CrucibleContext context(Object[] args) { - Object rawContext = args != null && args.length > 0 ? args[0] : null; - return new CrucibleContext(rawContext); - } - - private List runTests(CrucibleContext context) - throws ReflectiveOperationException { - - List results = new ArrayList<>(); - for (CrucibleTestCase test : suite.tests()) { - results.add(runTestSync(context, test)); - } - return results; - } - - private CompletableFuture> runTestsAsync(CrucibleContext context) { - CompletableFuture> results = CompletableFuture.completedFuture( - new ArrayList<>()); - for (CrucibleTestCase test : suite.tests()) { - results = results.thenCompose(current -> runTestAsync(context, test) - .thenApply(result -> { - current.add(result); - return current; - })); - } - return results; - } - - private Object runTestSync(CrucibleContext context, CrucibleTestCase test) - throws ReflectiveOperationException { - - try { - TestOutcome outcome = test.body().run(context).toCompletableFuture().join(); - if (outcome.passed()) { - return bridge.pass(suite.id(), test.name()); - } - return bridge.fail(suite.id(), - test.name(), - failureMessage(test, outcome)); - } catch (CompletionException e) { - return bridge.error(suite.id(), test.name(), asException(e)); - } catch (Exception e) { - return bridge.error(suite.id(), test.name(), e); - } - } - - private CompletableFuture runTestAsync(CrucibleContext context, - CrucibleTestCase test) { - try { - return test.body().run(context).handle((outcome, failure) -> { - try { - if (failure != null) { - return bridge.error(suite.id(), test.name(), asException(failure)); - } - if (outcome != null && outcome.passed()) { - return bridge.pass(suite.id(), test.name()); - } - return bridge.fail(suite.id(), - test.name(), - failureMessage(test, outcome)); - } catch (ReflectiveOperationException e) { - throw new CompletionException(e); - } - }).toCompletableFuture(); - } catch (Exception e) { - try { - return CompletableFuture.completedFuture( - bridge.error(suite.id(), test.name(), e)); - } catch (ReflectiveOperationException reflective) { - return CompletableFuture.failedFuture(reflective); - } - } - } - - private static String failureMessage(CrucibleTestCase test, - TestOutcome outcome) { - if (outcome != null && outcome.failureMessage() != null - && !outcome.failureMessage().isBlank()) { - return outcome.failureMessage(); - } - return test.failureMessage(); - } - - private static Exception asException(Throwable failure) { - Throwable current = failure; - if (current instanceof CompletionException && current.getCause() != null) { - current = current.getCause(); - } - if (current instanceof Exception exception) { - return exception; - } - return new RuntimeException(current); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleContext.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleContext.java deleted file mode 100644 index b547cf93..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleContext.java +++ /dev/null @@ -1,77 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.crucible; - -import com.hypixel.hytale.server.core.universe.world.World; -import java.lang.reflect.Method; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -/** - * Reflection wrapper around Crucible's TestContext. - */ -final class CrucibleContext { - - private static final long MILLIS_PER_TICK = 50L; - private static final ScheduledExecutorService DELAYED_WORLD_CALLBACKS = - Executors.newSingleThreadScheduledExecutor(runnable -> { - Thread thread = new Thread(runnable, "Impulse Crucible delayed callbacks"); - thread.setDaemon(true); - return thread; - }); - - private final Object handle; - - CrucibleContext(Object handle) { - this.handle = handle; - } - - World world() throws ReflectiveOperationException { - Method getWorld = handle.getClass().getMethod("getWorld"); - return (World) getWorld.invoke(handle); - } - - int wx(int value) throws ReflectiveOperationException { - return coordinate("wx", value); - } - - int wy(int value) throws ReflectiveOperationException { - return coordinate("wy", value); - } - - int wz(int value) throws ReflectiveOperationException { - return coordinate("wz", value); - } - - @SuppressWarnings("unchecked") - CompletionStage waitTicks(int ticks) throws ReflectiveOperationException { - Method waitTicks = handle.getClass().getMethod("waitTicks", int.class); - return (CompletionStage) waitTicks.invoke(handle, ticks); - } - - /** - * Waits approximately long enough for the test world to tick, then resumes on - * the world thread with one queued task. Crucible's built-in waitTicks helper - * requeues itself through World.execute while the same task queue is draining, - * which prevents the world tick counter from advancing on current Hytale builds. - */ - CompletionStage waitApproxTicksOnWorld(int ticks) throws ReflectiveOperationException { - World world = world(); - CompletableFuture future = new CompletableFuture<>(); - long delayMillis = Math.max(1L, ticks) * MILLIS_PER_TICK; - DELAYED_WORLD_CALLBACKS.schedule(() -> { - try { - world.execute(() -> future.complete(null)); - } catch (Exception e) { - future.completeExceptionally(e); - } - }, delayMillis, TimeUnit.MILLISECONDS); - return future; - } - - private int coordinate(String methodName, int value) throws ReflectiveOperationException { - Method method = handle.getClass().getMethod(methodName, int.class); - return (int) method.invoke(handle, value); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleSuite.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleSuite.java deleted file mode 100644 index 16a98b88..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleSuite.java +++ /dev/null @@ -1,15 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.crucible; - -import java.util.List; -import java.util.Set; - -/** - * Backend-neutral description of one Crucible suite. - */ -record CrucibleSuite( - String id, - String name, - String description, - Set tags, - List tests) { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleTestCase.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleTestCase.java deleted file mode 100644 index b592d2d0..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleTestCase.java +++ /dev/null @@ -1,65 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.crucible; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - -/** - * One proxy-backed Crucible test case. - */ -record CrucibleTestCase( - String name, - ContextualTest body, - String failureMessage) { - - static CrucibleTestCase sync(String name, BooleanTest body, String failureMessage) { - return new CrucibleTestCase(name, - context -> CompletableFuture.completedFuture(TestOutcome.from(body.run())), - failureMessage); - } - - static CrucibleTestCase async(String name, - ContextualBooleanTest body, - String failureMessage) { - - return new CrucibleTestCase(name, - context -> body.run(context).thenApply(TestOutcome::from), - failureMessage); - } - - static CrucibleTestCase asyncResult(String name, - ContextualTest body, - String failureMessage) { - - return new CrucibleTestCase(name, body, failureMessage); - } - - @FunctionalInterface - interface BooleanTest { - boolean run(); - } - - @FunctionalInterface - interface ContextualBooleanTest { - CompletionStage run(CrucibleContext context); - } - - @FunctionalInterface - interface ContextualTest { - CompletionStage run(CrucibleContext context); - } - - record TestOutcome(boolean passed, String failureMessage) { - - static TestOutcome pass() { - return new TestOutcome(true, ""); - } - - static TestOutcome fail(String failureMessage) { - return new TestOutcome(false, failureMessage); - } - - static TestOutcome from(boolean passed) { - return passed ? pass() : fail(""); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ImpulseApiCrucibleTests.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ImpulseApiCrucibleTests.java deleted file mode 100644 index afb1d4ba..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ImpulseApiCrucibleTests.java +++ /dev/null @@ -1,479 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.crucible; - -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; -import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; -import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodySnapshots; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.query.SpaceBodyCountQuery; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsBackendExtensionId; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.plugin.settings.VisualOcclusionMode; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionMode; -import java.util.Collection; -import java.util.List; -import java.util.Set; -import javax.annotation.Nonnull; - -/** - * Crucible suites that exercise Impulse API behavior inside a live Hytale server. - */ -final class ImpulseApiCrucibleTests { - - private static final PhysicsBackendExtensionId RAPIER_SOLVER_EXTENSION_ID = - new PhysicsBackendExtensionId("impulse:rapier_solver"); - private static final String RAPIER_INTERNAL_PGS_ITERATIONS = "internalPgsIterations"; - private static final String RAPIER_MIN_ISLAND_SIZE = "minIslandSize"; - - private ImpulseApiCrucibleTests() { - } - - static void register(CrucibleBridge bridge, ClassLoader loader) - throws ReflectiveOperationException { - - bridge.registerSuite(loader, smokeSuite()); - bridge.registerSuite(loader, runtimeStabilitySuite()); - bridge.registerSuite(loader, transformSyncSuite()); - bridge.registerSuite(loader, terrainCollisionSuite()); - } - - private static CrucibleSuite smokeSuite() { - return new CrucibleSuite( - "impulse:smoke", - "Impulse Smoke", - "Verifies plugin load, backend registration, and basic API operations", - Set.of("smoke"), - List.of( - CrucibleTestCase.sync("plugin loaded", () -> ImpulsePlugin.get() != null, - "ImpulsePlugin singleton is null"), - CrucibleTestCase.sync("backends registered", () -> { - Collection providers = Impulse.getRuntimeProviders(); - return !providers.isEmpty(); - }, "No physics backend runtimes registered"), - CrucibleTestCase.sync("test backend selectable", () -> - { - CrucibleBackends.requireBackendId(); - return true; - }, - "No backend id is available for Crucible tests"), - CrucibleTestCase.asyncResult("world collision subplugin load/unload/reload", - ignored -> WorldCollisionSubPluginCrucibleSupport.loadUnloadReloadSmokeAsync(), - "World collision subplugin lifecycle smoke failed"), - CrucibleTestCase.asyncResult("control subplugin load/unload/reload", - ControlSubPluginCrucibleSupport::loadUnloadReloadSmokeAsync, - "Control subplugin lifecycle smoke failed"), - CrucibleTestCase.sync("create space and body", - ImpulseApiCrucibleTests::createSpaceAndBody, - "Expected a body to be added to the physics space"), - CrucibleTestCase.sync("step", - ImpulseApiCrucibleTests::stepSpaceDoesNotThrow, - "Physics space step failed"))); - } - - private static CrucibleSuite runtimeStabilitySuite() { - return new CrucibleSuite( - "impulse:runtime_stability", - "Impulse Runtime Stability", - "Verifies explicit space lifecycle, detached cleanup, and settings retention", - Set.of("smoke", "stability"), - List.of( - CrucibleTestCase.sync("fresh world has no spaces", - ImpulseApiCrucibleTests::freshWorldHasNoSpaces, - "Fresh PhysicsWorldResource unexpectedly has spaces"), - CrucibleTestCase.sync("created explicit space lifecycle works", - ImpulseApiCrucibleTests::createdExplicitSpaceLifecycleWorks, - "Explicit space was not registered correctly"), - CrucibleTestCase.sync("clear populated spaces", - ImpulseApiCrucibleTests::clearPopulatedSpaces, - "clearAllSpaces did not remove populated runtime spaces"), - CrucibleTestCase.sync("detached unregister removes body", - ImpulseApiCrucibleTests::detachedUnregisterRemovesBackendBody, - "Detached unregister did not remove the backend body"), - CrucibleTestCase.sync("settings round trip", - ImpulseApiCrucibleTests::settingsRoundTrip, - "PhysicsSpaceSettings did not retain runtime settings"))); - } - - private static CrucibleSuite transformSyncSuite() { - return new CrucibleSuite( - "impulse:transform_sync", - "Impulse Transform Sync", - "Verifies physics bodies move correctly under gravity", - Set.of("integration"), - List.of( - CrucibleTestCase.sync("body fell", ImpulseApiCrucibleTests::dynamicBodyFalls, - "Dynamic body did not fall under gravity"), - CrucibleTestCase.sync("static body stays", - ImpulseApiCrucibleTests::staticBodyDoesNotMove, - "Static body moved while stepping the physics space"))); - } - - private static CrucibleSuite terrainCollisionSuite() { - return new CrucibleSuite( - "impulse:terrain_collision", - "Impulse Terrain Collision", - "Verifies dynamic bodies land on ground plane collision", - Set.of("collision"), - List.of( - CrucibleTestCase.sync("body landed", - ImpulseApiCrucibleTests::bodyLandsOnGroundPlane, - "Body did not land on the ground plane"), - CrucibleTestCase.sync("body settled", - ImpulseApiCrucibleTests::bodySettlesWithinTolerance, - "Body did not settle within the expected speed tolerance"), - CrucibleTestCase.sync("ray hit ground", - ImpulseApiCrucibleTests::raycastHitsGroundPlane, - "Raycast from above did not hit the ground plane"))); - } - - private static boolean createSpaceAndBody() { - PhysicsBackendRuntime runtime = Impulse.createRuntime(CrucibleBackends.requireBackendId()); - int spaceId = runtime.createSpace(SpaceId.next()); - try { - long bodyId = runtime.createBody(spaceId, - BackendRuntimeCodes.SHAPE_BOX, - 0.5f, - 0.5f, - 0.5f, - 0.0f, - 0.0f, - BackendRuntimeCodes.AXIS_Y, - 0.0f, - 1.0f, - BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC), - 0f, - 10f, - 0f, - 0.0f, - 0.0f, - 0.0f, - 1.0f); - return runtime.bodyCount(spaceId) == 1 && runtime.containsBody(spaceId, bodyId); - } finally { - runtime.destroySpace(spaceId); - } - } - - private static boolean freshWorldHasNoSpaces() { - PhysicsWorldResource resource = new PhysicsWorldRuntimeResource(); - return resource.getSpaceIds().isEmpty(); - } - - private static boolean createdExplicitSpaceLifecycleWorks() { - PhysicsWorldResource resource = new PhysicsWorldRuntimeResource(); - SpaceId spaceId = resource.createSpace(CrucibleBackends.requireBackendId(), - "crucible", - PhysicsSpaceSettings.streamingWorldCollision()); - try { - return resource.hasSpace(spaceId) - && resource.getSpaceSettings(spaceId).getWorldCollisionSettings().getWorldCollisionMode() - == WorldCollisionMode.STREAMING; - } finally { - resource.clearAllSpaces("crucible"); - } - } - - private static boolean clearPopulatedSpaces() { - PhysicsWorldResource resource = new PhysicsWorldRuntimeResource(); - SpaceId spaceId = resource.createSpace(CrucibleBackends.requireBackendId(), - "crucible", - PhysicsSpaceSettings.defaults()); - submitCrucibleBox(resource, spaceId, RigidBodyKey.random()); - - resource.clearAllSpaces("crucible"); - return resource.getSpaceIds().isEmpty(); - } - - private static boolean detachedUnregisterRemovesBackendBody() { - PhysicsWorldResource resource = new PhysicsWorldRuntimeResource(); - SpaceId spaceId = resource.createSpace(CrucibleBackends.requireBackendId(), - "crucible", - PhysicsSpaceSettings.defaults()); - try { - RigidBodyKey bodyKey = RigidBodyKey.random(); - submitCrucibleBox(resource, spaceId, bodyKey); - - resource.destroyBody(bodyKey); - boolean spaceEmpty = resource.query(new SpaceBodyCountQuery(spaceId)) - .completion() - .toCompletableFuture() - .join() == 0; - return spaceEmpty && resource.getBodyRegistrationViews().isEmpty(); - } finally { - resource.clearAllSpaces("crucible"); - } - } - - private static void submitCrucibleBox(@Nonnull PhysicsWorldResource resource, - @Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyKey) { - resource.submitCommands(0L, - 1, - commands -> commands.spawnBody(bodyKey, spawn -> spawn - .space(spaceId) - .shape(PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f)) - .mass(1.0f) - .type(PhysicsBodyType.DYNAMIC) - .position(0f, 5f, 0f) - .settings(RigidBodySpawnSettings.defaults()) - .kind(PhysicsBodyKind.BODY) - .persistence(PhysicsBodyPersistenceMode.RUNTIME_ONLY))) - .firstRejected() - .toCompletableFuture() - .join() - .ifPresent(result -> { - throw new IllegalStateException("spawn crucible box command " - + result.commandSequence() + " rejected: " + result.message()); - }); - } - - private static boolean settingsRoundTrip() { - PhysicsWorldResource resource = new PhysicsWorldRuntimeResource(); - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - settings.getWorldCollisionSettings().setWorldCollisionMode(WorldCollisionMode.STREAMING); - settings.getWorldCollisionSettings().setWorldCollisionRadius(9); - settings.getWorldCollisionSettings().setWorldCollisionBodyRadius(5); - settings.getWorldCollisionSettings().setWorldCollisionTtlTicks(77); - settings.getVisualSyncSettings().setVisualMaxSyncRadius(96); - settings.getVisualSyncSettings().setVisualFullSyncRadius(48); - settings.getVisualSyncSettings().setVisualFarSyncCutoffEnabled(false); - settings.getVisualSyncSettings().setVisualMidSyncIntervalTicks(3); - settings.getVisualSyncSettings().setVisualFarSyncIntervalTicks(17); - settings.getVisualSyncSettings().setVisualOcclusionMode(VisualOcclusionMode.PRIORITY); - settings.getVisualSyncSettings().setVisualOcclusionRaycastsPerTick(31); - settings.getVisualSyncSettings().setVisualOcclusionCacheTicks(7); - settings.getSolverSettings().setSolverIterations(5); - settings.getSolverSettings().setStabilizationIterations(1); - settings.getExtensionSettings().setInt(RAPIER_SOLVER_EXTENSION_ID, - RAPIER_INTERNAL_PGS_ITERATIONS, - 2); - settings.getExtensionSettings().setInt(RAPIER_SOLVER_EXTENSION_ID, - RAPIER_MIN_ISLAND_SIZE, - 64); - settings.getVisualSyncSettings().setEntityVisualSyncCullingEnabled(true); - settings.getVisualSyncSettings().setVisualVisibilityCullingEnabled(true); - settings.getVisualMaterializationSettings().setDetachedVisualMaterializationEnabled(true); - settings.getVisualMaterializationSettings().setDetachedVisualDematerializationRadius(72); - settings.getVisualMaterializationSettings().setDetachedVisualMaterializationRadius(48); - settings.getVisualMaterializationSettings().setDetachedVisualMaxSpawnsPerTick(33); - settings.getVisualMaterializationSettings().setDetachedVisualMaxMaterialized(444); - settings.getVisualMaterializationSettings().setDetachedVisualBlockType("Rock_Stone"); - - SpaceId spaceId = resource.createSpace(CrucibleBackends.requireBackendId(), - "crucible", - settings); - try { - PhysicsSpaceSettings copy = resource.getSpaceSettings(spaceId); - return copy.getWorldCollisionSettings().getWorldCollisionMode() == WorldCollisionMode.STREAMING - && copy.getWorldCollisionSettings().getWorldCollisionRadius() == 9 - && copy.getWorldCollisionSettings().getWorldCollisionBodyRadius() == 5 - && copy.getWorldCollisionSettings().getWorldCollisionTtlTicks() == 77 - && copy.getVisualSyncSettings().getVisualFullSyncRadius() == 48 - && copy.getVisualSyncSettings().getVisualMaxSyncRadius() == 96 - && !copy.getVisualSyncSettings().isVisualFarSyncCutoffEnabled() - && copy.getVisualSyncSettings().getVisualMidSyncIntervalTicks() == 3 - && copy.getVisualSyncSettings().getVisualFarSyncIntervalTicks() == 17 - && copy.getVisualSyncSettings().getVisualOcclusionMode() == VisualOcclusionMode.PRIORITY - && copy.getVisualSyncSettings().getVisualOcclusionRaycastsPerTick() == 31 - && copy.getVisualSyncSettings().getVisualOcclusionCacheTicks() == 7 - && copy.getSolverSettings().getSolverIterations() == 5 - && copy.getSolverSettings().getStabilizationIterations() == 1 - && copy.getExtensionSettings() - .getInt(RAPIER_SOLVER_EXTENSION_ID, - RAPIER_INTERNAL_PGS_ITERATIONS) - .orElse(-1) == 2 - && copy.getExtensionSettings() - .getInt(RAPIER_SOLVER_EXTENSION_ID, - RAPIER_MIN_ISLAND_SIZE) - .orElse(-1) == 64 - && copy.getVisualSyncSettings().isEntityVisualSyncCullingEnabled() - && copy.getVisualSyncSettings().isVisualVisibilityCullingEnabled() - && copy.getVisualMaterializationSettings().isDetachedVisualMaterializationEnabled() - && copy.getVisualMaterializationSettings().getDetachedVisualMaterializationRadius() == 48 - && copy.getVisualMaterializationSettings().getDetachedVisualDematerializationRadius() == 72 - && copy.getVisualMaterializationSettings().getDetachedVisualMaxSpawnsPerTick() == 33 - && copy.getVisualMaterializationSettings().getDetachedVisualMaxMaterialized() == 444 - && "Rock_Stone".equals(copy.getVisualMaterializationSettings().getDetachedVisualBlockType()); - } finally { - resource.clearAllSpaces("crucible"); - } - } - - private static boolean stepSpaceDoesNotThrow() { - PhysicsBackendRuntime runtime = Impulse.createRuntime(CrucibleBackends.requireBackendId()); - int spaceId = runtime.createSpace(SpaceId.next()); - try { - runtime.setGravity(spaceId, 0f, -9.81f, 0f); - createBox(runtime, spaceId, 1.0f, PhysicsBodyType.DYNAMIC, 0f, 10f, 0f); - runtime.step(spaceId, 1f / 60f); - return true; - } finally { - runtime.destroySpace(spaceId); - } - } - - private static boolean dynamicBodyFalls() { - PhysicsBackendRuntime runtime = Impulse.createRuntime(CrucibleBackends.requireBackendId()); - int spaceId = runtime.createSpace(SpaceId.next()); - try { - runtime.setGravity(spaceId, 0f, -9.81f, 0f); - long bodyId = createBox(runtime, spaceId, 1.0f, PhysicsBodyType.DYNAMIC, 0f, 20f, 0f); - - float dt = 1f / 60f; - for (int i = 0; i < 60; i++) { - runtime.step(spaceId, dt); - } - - PhysicsBodySnapshot snapshot = requireSnapshot(runtime, spaceId, bodyId); - float yAfter = snapshot.positionY(); - float vy = snapshot.linearVelocityY(); - return yAfter < 20f && vy < 0f; - } finally { - runtime.destroySpace(spaceId); - } - } - - private static boolean staticBodyDoesNotMove() { - PhysicsBackendRuntime runtime = Impulse.createRuntime(CrucibleBackends.requireBackendId()); - int spaceId = runtime.createSpace(SpaceId.next()); - try { - runtime.setGravity(spaceId, 0f, -9.81f, 0f); - long bodyId = createBox(runtime, spaceId, 0.0f, PhysicsBodyType.STATIC, 0f, 10f, 0f); - - float dt = 1f / 60f; - for (int i = 0; i < 60; i++) { - runtime.step(spaceId, dt); - } - - float yAfter = requireSnapshot(runtime, spaceId, bodyId).positionY(); - return Math.abs(yAfter - 10f) < 0.01f; - } finally { - runtime.destroySpace(spaceId); - } - } - - private static boolean bodyLandsOnGroundPlane() { - PhysicsBackendRuntime runtime = Impulse.createRuntime(CrucibleBackends.requireBackendId()); - int spaceId = runtime.createSpace(SpaceId.next()); - try { - runtime.setGravity(spaceId, 0f, -9.81f, 0f); - createPlane(runtime, spaceId, 0f); - - long bodyId = createBox(runtime, spaceId, 1.0f, PhysicsBodyType.DYNAMIC, 0f, 5f, 0f); - - float dt = 1f / 60f; - for (int i = 0; i < 300; i++) { - runtime.step(spaceId, dt); - } - - float yAfter = requireSnapshot(runtime, spaceId, bodyId).positionY(); - return yAfter < 3f && yAfter > -0.5f; - } finally { - runtime.destroySpace(spaceId); - } - } - - private static boolean bodySettlesWithinTolerance() { - PhysicsBackendRuntime runtime = Impulse.createRuntime(CrucibleBackends.requireBackendId()); - int spaceId = runtime.createSpace(SpaceId.next()); - try { - runtime.setGravity(spaceId, 0f, -9.81f, 0f); - createPlane(runtime, spaceId, 0f); - - long bodyId = createBox(runtime, spaceId, 1.0f, PhysicsBodyType.DYNAMIC, 0f, 2f, 0f); - - float dt = 1f / 60f; - for (int i = 0; i < 300; i++) { - runtime.step(spaceId, dt); - } - - float speed = requireSnapshot(runtime, spaceId, bodyId).linearVelocity().length(); - return speed < 0.5f; - } finally { - runtime.destroySpace(spaceId); - } - } - - private static boolean raycastHitsGroundPlane() { - PhysicsBackendRuntime runtime = Impulse.createRuntime(CrucibleBackends.requireBackendId()); - int spaceId = runtime.createSpace(SpaceId.next()); - try { - createPlane(runtime, spaceId, 0f); - - return runtime.raycastAll(spaceId, 0f, 10f, 0f, 0f, -10f, 0f, (_, _, _, _, _, _, _, _, _) -> { - }) > 0; - } finally { - runtime.destroySpace(spaceId); - } - } - - @Nonnull - private static PhysicsBodySnapshot requireSnapshot(@Nonnull PhysicsBackendRuntime runtime, - int spaceId, - long bodyId) { - PhysicsBodySnapshot snapshot = PhysicsBodySnapshots.read(runtime, spaceId, bodyId); - if (snapshot == null) { - throw new IllegalStateException("Missing backend snapshot for body " + bodyId); - } - return snapshot; - } - - private static long createBox(@Nonnull PhysicsBackendRuntime runtime, - int spaceId, - float mass, - @Nonnull PhysicsBodyType bodyType, - float positionX, - float positionY, - float positionZ) { - return runtime.createBody(spaceId, - BackendRuntimeCodes.SHAPE_BOX, - 0.5f, - 0.5f, - 0.5f, - 0.0f, - 0.0f, - BackendRuntimeCodes.AXIS_Y, - 0.0f, - mass, - BackendRuntimeCodes.bodyTypeCode(bodyType), - positionX, - positionY, - positionZ, - 0.0f, - 0.0f, - 0.0f, - 1.0f); - } - - private static long createPlane(@Nonnull PhysicsBackendRuntime runtime, int spaceId, float groundY) { - return runtime.createBody(spaceId, - BackendRuntimeCodes.SHAPE_PLANE, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - BackendRuntimeCodes.AXIS_Y, - groundY, - 0.0f, - BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.STATIC), - 0.0f, - groundY, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 1.0f); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ImpulseCrucibleSuites.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ImpulseCrucibleSuites.java deleted file mode 100644 index beb10951..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ImpulseCrucibleSuites.java +++ /dev/null @@ -1,26 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.crucible; - -/** - * Entry point used by the core plugin to register optional Crucible suites. - */ -public final class ImpulseCrucibleSuites { - - private ImpulseCrucibleSuites() { - } - - /** - * Registers all Impulse suites using the classloader that loaded Crucible. - * - * @param crucibleLoader the classloader for the Crucible plugin - * @throws ReflectiveOperationException if Crucible's API shape changed - */ - public static void register(ClassLoader crucibleLoader) - throws ReflectiveOperationException { - - CrucibleBridge bridge = CrucibleBridge.create(crucibleLoader); - ImpulseApiCrucibleTests.register(bridge, crucibleLoader); - ImpulseLiveCrucibleTests.register(bridge, crucibleLoader); - ImpulseDetachedStreamingBenchmarkCrucibleTests.register(bridge, crucibleLoader); - ImpulseRapierBodyBenchmarkCrucibleTests.register(bridge, crucibleLoader); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ImpulseDetachedStreamingBenchmarkCrucibleTests.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ImpulseDetachedStreamingBenchmarkCrucibleTests.java deleted file mode 100644 index 4473443c..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ImpulseDetachedStreamingBenchmarkCrucibleTests.java +++ /dev/null @@ -1,920 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.crucible; - -import com.hypixel.hytale.component.Archetype; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.logger.HytaleLogger; -import com.hypixel.hytale.math.util.ChunkUtil; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.chunk.ChunkFlag; -import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk; -import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource.StepSnapshot; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource.SyncSnapshot; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource.Snapshot; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.simulation.query.BenchmarkSpaceStatsQuery; -import dev.hytalemodding.impulse.core.internal.simulation.view.BenchmarkSpaceStatsView; -import dev.hytalemodding.impulse.core.internal.simulation.query.WorldCollisionPrewarmEnvelopeQuery; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionPrewarmStats; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsBackendExtensionId; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepSchedulingMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionMode; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import it.unimi.dsi.fastutil.longs.LongOpenHashSet; -import it.unimi.dsi.fastutil.longs.LongSet; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.CompletionStage; -import java.util.logging.Level; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3d; - -/** - * Benchmark-oriented Crucible scenario for detached bodies using streamed world collision. - */ -@SuppressWarnings("SameParameterValue") -final class ImpulseDetachedStreamingBenchmarkCrucibleTests { - - private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); - private static final String COUNTS_PROPERTY = "impulse.crucible.detachedStreaming.counts"; - private static final String WARMUP_TICKS_PROPERTY = - "impulse.crucible.detachedStreaming.warmupTicks"; - private static final String SAMPLE_TICKS_PROPERTY = - "impulse.crucible.detachedStreaming.sampleTicks"; - private static final String MIN_TPS_PROPERTY = - "impulse.crucible.detachedStreaming.minTps"; - private static final String WARN_TPS_PROPERTY = - "impulse.crucible.detachedStreaming.warnTps"; - private static final String STRICT_PLANE_GATE_PROPERTY = - "impulse.crucible.detachedStreaming.strictPlaneGate"; - private static final PhysicsBackendExtensionId RAPIER_SOLVER_EXTENSION_ID = - new PhysicsBackendExtensionId("impulse:rapier_solver"); - private static final String RAPIER_INTERNAL_PGS_ITERATIONS = "internalPgsIterations"; - private static final String RAPIER_MIN_ISLAND_SIZE = "minIslandSize"; - - private static final int DEFAULT_STAGE_COUNT = 500; - private static final int DEFAULT_WARMUP_TICKS = 60; - private static final int DEFAULT_SAMPLE_TICKS = 200; - private static final int MIN_STAGE_COUNT = 1; - private static final int MAX_STAGE_COUNT = 10_000; - private static final int MIN_WARMUP_TICKS = 1; - private static final int MAX_WARMUP_TICKS = 1_200; - private static final int MIN_SAMPLE_TICKS = 20; - private static final int MAX_SAMPLE_TICKS = 7_200; - private static final int BODY_STREAMING_RADIUS = 8; - private static final int TICKING_CHUNK_REQUEST_FLAGS = 4; - private static final int MAX_CHUNK_PREFLIGHT_ATTEMPTS = 100; - private static final int CHUNK_PREFLIGHT_WAIT_TICKS = 2; - private static final float TARGET_MAX_STEP_DT = 1.0f / 30.0f; - private static final float GROUND_Y = 122.0f; - private static final float BELOW_PLANE_TOLERANCE = 1.0f; - private static final float BODY_WORLD_MIN_Y = -32.0f; - private static final float BODY_VOID_Y = -128.0f; - private static final double STREAMING_FALL_ENVELOPE_MIN_Y = 0.0; - private static final double STREAMING_HORIZONTAL_DRIFT_HALO_BLOCKS = 16.0; - private static final double DETACHED_SPACING = 1.5; - private static final Vector3d ORIGIN = new Vector3d(0.0, 128.0, 0.0); - private static final ComponentType WORLD_CHUNK_TYPE = - WorldChunk.getComponentType(); - - private ImpulseDetachedStreamingBenchmarkCrucibleTests() { - } - - static void register(CrucibleBridge bridge, ClassLoader loader) - throws ReflectiveOperationException { - - bridge.registerSuite(loader, benchmarkSuite()); - } - - private static CrucibleSuite benchmarkSuite() { - return new CrucibleSuite( - "impulse:detached_streaming_benchmark", - "Impulse Detached Streaming Benchmark", - "Runs detached full-collision streamed-world benchmark stages with health gates", - Set.of("benchmark", "streaming"), - List.of(CrucibleTestCase.asyncResult("detached full-collision streaming stages", - ImpulseDetachedStreamingBenchmarkCrucibleTests::detachedStreamingStages, - "Detached streaming benchmark breached a health gate"))); - } - - private static CompletionStage detachedStreamingStages( - CrucibleContext context) { - try { - StagePlan plan = StagePlan.fromSystemProperties(); - StageRunner runner = new StageRunner(context, plan); - return runner.run(); - } catch (ReflectiveOperationException e) { - return CompletableFuture.failedFuture(e); - } - } - - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - private static final class StageRunner { - - private final CrucibleContext context; - private final StagePlan plan; - private final World world; - private final PhysicsWorldRuntimeResource physics; - private final PhysicsRuntimeProfilingResource runtimeProfiling; - private final WorldCollisionProfilingResource worldCollisionProfiling; - private final PhysicsWorldSettings previousWorldSettings; - private final List retainedChunks = new ArrayList<>(); - - private StageRunner(@Nonnull CrucibleContext context, @Nonnull StagePlan plan) - throws ReflectiveOperationException { - this.context = context; - this.plan = plan; - this.world = context.world(); - Store store = world.getEntityStore().getStore(); - this.physics = PhysicsWorldRuntimeResource.require(store); - this.runtimeProfiling = store.getResource(PhysicsRuntimeProfilingResource.getResourceType()); - this.worldCollisionProfiling = store.getResource( - WorldCollisionProfilingResource.getResourceType()); - this.previousWorldSettings = physics.getWorldSettings(); - } - - private CompletionStage run() { - return runStage(0, new ArrayList<>()).handle((outcome, failure) -> { - clearStageState(); - restoreStepSettings(); - if (failure != null) { - if (failure instanceof CompletionException completionException) { - throw completionException; - } - throw new CompletionException(failure); - } - return outcome; - }); - } - - private CompletionStage runStage(int stageIndex, - List reports) { - if (stageIndex >= plan.counts().size()) { - return CompletableFuture.completedFuture(outcome(reports)); - } - - int count = plan.counts().get(stageIndex); - return startStageWhenReady(count, 1) - .thenCompose(started -> contextWait(plan.warmupTicks()).thenCompose(_ -> { - runtimeProfiling.reset(); - worldCollisionProfiling.reset(); - runtimeProfiling.setEnabled(true); - worldCollisionProfiling.setEnabled(true); - long startedNanos = System.nanoTime(); - return contextWait(plan.sampleTicks()).thenApply( - _ -> finishStage(count, started, startedNanos)); - })) - .thenCompose(report -> { - reports.add(report); - LOGGER.at(Level.INFO).log("Crucible detached streaming stage: %s", - report.summary()); - clearStageState(); - if (report.health().status() == StageStatus.STOP) { - return CompletableFuture.completedFuture(outcome(reports)); - } - return runStage(stageIndex + 1, reports); - }); - } - - private CompletionStage startStageWhenReady(int count, int attempt) { - clearStageState(); - if (!WorldCollisionSubPluginCrucibleSupport.ensureLoaded()) { - return CompletableFuture.completedFuture(StartedStage.failed(count, - "world collision subplugin did not load")); - } - - PhysicsWorldSettings worldSettings = physics.getWorldSettings(); - worldSettings.setStepMode(PhysicsStepMode.PROGRESSIVE_REFINEMENT); - worldSettings.setStepSchedulingMode(PhysicsStepSchedulingMode.DROP_PENDING_DT); - worldSettings.setSimulationSteps(1); - worldSettings.setMaxStepDt(TARGET_MAX_STEP_DT); - physics.setWorldSettings(worldSettings); - - BenchmarkChunks chunks = benchmarkChunks(count); - if (!areChunksReady(chunks)) { - int requested = requestChunks(chunks); - if (attempt >= MAX_CHUNK_PREFLIGHT_ATTEMPTS) { - String message = "count=" + count - + " chunk preflight failed after " + attempt - + " attempts; refs=" + chunks.size() - + " requested=" + requested; - return CompletableFuture.completedFuture( - StartedStage.failed(count, message)); - } - return contextWait(CHUNK_PREFLIGHT_WAIT_TICKS) - .thenCompose(_ -> startStageWhenReady(count, attempt + 1)); - } - - int retained = retainChunks(chunks); - configureMissingSectionDiagnostics(chunks); - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - settings.getWorldCollisionSettings().setWorldCollisionMode(WorldCollisionMode.STREAMING); - settings.getWorldCollisionSettings().setWorldCollisionBodyRadius(BODY_STREAMING_RADIUS); - settings.getSolverSettings().setSolverIterations(4); - settings.getSolverSettings().setStabilizationIterations(1); - settings.getExtensionSettings().setInt(RAPIER_SOLVER_EXTENSION_ID, - RAPIER_INTERNAL_PGS_ITERATIONS, - 1); - settings.getExtensionSettings().setInt(RAPIER_SOLVER_EXTENSION_ID, - RAPIER_MIN_ISLAND_SIZE, - 128); - - SpaceId spaceId = physics.createSpace(CrucibleBackends.requireBackendId(), - world.getName(), - settings); - PrewarmStats prewarm = prewarmWorldCollision(spaceId, count); - spawnDetachedBodies(spaceId, count); - runtimeProfiling.reset(); - worldCollisionProfiling.reset(); - runtimeProfiling.setEnabled(true); - worldCollisionProfiling.setEnabled(true); - return CompletableFuture.completedFuture( - StartedStage.started(spaceId, chunks, retained, prewarm)); - } - - private StageReport finishStage(int count, - StartedStage started, - long startedNanos) { - if (!started.started()) { - return StageReport.failedPreflight(count, started.failureMessage()); - } - SpaceId spaceId = started.spaceId(); - if (spaceId == null || !physics.hasSpace(spaceId)) { - return StageReport.failedPreflight(count, "space disappeared during benchmark"); - } - - StepSnapshot step = runtimeProfiling.getCumulativeStep(); - SyncSnapshot sync = runtimeProfiling.getCumulativeSync(); - Snapshot worldCollision = worldCollisionProfiling.getCumulativeSnapshot(); - double elapsedSeconds = Math.max(0.001, - (System.nanoTime() - startedNanos) / 1_000_000_000.0); - double observedTickRate = step.getTickSamples() / elapsedSeconds; - SpaceStats stats = SpaceStats.collect(physics, spaceId); - double avgStepMs = averageMillis(step.getTickNanos(), step.getTickSamples()); - double avgSnapshotMs = averageMillis(step.getSnapshotNanos(), step.getTickSamples()); - double avgSyncMs = averageMillis(sync.getTickNanos(), sync.getTickSamples()); - double avgWorldMs = averageMillis(worldCollision.getTickNanos(), - worldCollision.getTickSamples()); - double totalMs = avgStepMs + avgSnapshotMs + avgSyncMs + avgWorldMs; - StageHealth health = assessHealth(count, - observedTickRate, - stats, - worldCollision.getMissingChunks()); - - assert started.chunks() != null; - assert started.prewarm() != null; - return new StageReport(count, - observedTickRate, - avgStepMs, - avgSnapshotMs, - avgSyncMs, - avgWorldMs, - totalMs, - started.retainedColumns(), - started.chunks().size(), - started.prewarm().sectionTargets(), - started.prewarm().sectionsBuilt(), - stats.bodies, - stats.dynamicBodies, - stats.worldCollisionBodies, - stats.belowPlaneBodies, - stats.belowTerrainBodies, - stats.belowWorldMinBodies, - stats.belowVoidBodies, - stats.terrainBaselineBodies, - stats.missingTerrainBaselineBodies, - stats.minTerrainBottomClearance(), - worldCollision.getTickSamples(), - worldCollision.getEnsureCalls(), - worldCollision.getSectionRequests(), - worldCollision.getSectionCacheHits(), - worldCollision.getSectionsBuilt(), - worldCollision.getMissingChunks(), - worldCollision.getMissingBlockChunks(), - worldCollision.getMissingBlockSections(), - worldCollision.getUniqueMissingSections(), - worldCollision.getMissingOutsideRetainedEnvelope(), - worldCollision.getBodyStreamingTargets(), - health); - } - - private CompletionStage contextWait(int ticks) { - try { - return context.waitApproxTicksOnWorld(ticks); - } catch (ReflectiveOperationException e) { - return CompletableFuture.failedFuture(e); - } - } - - private void clearStageState() { - releaseRetainedChunks(); - physics.clearBodies(); - physics.clearAllSpaces(world.getName()); - runtimeProfiling.reset(); - worldCollisionProfiling.reset(); - worldCollisionProfiling.clearDiagnosticRetainedSections(); - } - - private void restoreStepSettings() { - physics.setWorldSettings(previousWorldSettings); - } - - private PrewarmStats prewarmWorldCollision(@Nonnull SpaceId spaceId, int count) { - BenchmarkLayout layout = BenchmarkLayout.flatGrid(count); - WorldCollisionPrewarmStats stats = physics.queryInternal(new WorldCollisionPrewarmEnvelopeQuery(world, - spaceId, - count, - (float) layout.origin().x, - (float) layout.origin().y, - (float) layout.origin().z, - layout.side(), - (float) layout.spacing(), - BODY_STREAMING_RADIUS, - (float) STREAMING_FALL_ENVELOPE_MIN_Y, - (float) STREAMING_HORIZONTAL_DRIFT_HALO_BLOCKS, - 0L)) - .toCompletableFuture() - .join(); - return new PrewarmStats(stats.sectionTargets(), - stats.buildStats().sectionsBuilt(), - stats.buildStats().colliderBodies()); - } - - private void configureMissingSectionDiagnostics(@Nonnull BenchmarkChunks chunks) { - LongSet sectionKeys = new LongOpenHashSet(); - for (ChunkSection section : chunks.sections()) { - sectionKeys.add(WorldCollisionProfilingResource.packDiagnosticSectionKey( - section.x(), - section.y(), - section.z())); - } - worldCollisionProfiling.setDiagnosticRetainedSections(sectionKeys); - } - - private void spawnDetachedBodies(@Nonnull SpaceId spaceId, int count) { - BenchmarkLayout layout = BenchmarkLayout.flatGrid(count); - PhysicsShapeSpec box = PhysicsShapeSpec.box(0.48f, 0.48f, 0.48f); - RigidBodySpawnSettings settings = RigidBodySpawnSettings.of(0.45f, - 0.0f, - 0.02f, - 0.25f, - PhysicsCollisionFilters.DYNAMIC_BODY, - PhysicsCollisionFilters.TERRAIN | PhysicsCollisionFilters.DYNAMIC_BODY); - long bodyKeyRunId = RigidBodyKey.random().mostSignificantBits(); - physics.submitCommands(Math.max(0L, world.getTick()), - 1, - commands -> commands.spawnBodies(count, - spaceId, - box, - 1.0f, - PhysicsBodyType.DYNAMIC, - settings, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY, - spawns -> { - for (int i = 0; i < count; i++) { - spawns.body(bodyKeyRunId, - i + 1L, - (float) layout.positionX(i), - (float) layout.positionY(), - (float) layout.positionZ(i)); - } - })) - .firstRejected() - .toCompletableFuture() - .join() - .ifPresent(result -> { - throw new IllegalStateException("spawn detached streaming benchmark bodies command " - + result.commandSequence() + " rejected: " + result.message()); - }); - } - - private BenchmarkChunks benchmarkChunks(int count) { - LongSet columns = new LongOpenHashSet(); - Set sections = new ObjectOpenHashSet<>(); - BenchmarkLayout layout = BenchmarkLayout.flatGrid(count); - for (int i = 0; i < count; i++) { - addStreamingCollisionChunks(columns, - sections, - layout.positionX(i), - layout.positionY(), - layout.positionZ(i)); - } - return new BenchmarkChunks(columns, sections); - } - - private void addStreamingCollisionChunks(@Nonnull LongSet columns, - @Nonnull Set sections, - double positionX, - double positionY, - double positionZ) { - int centerBlockX = (int) Math.floor(positionX); - int centerBlockZ = (int) Math.floor(positionZ); - int horizontalRadius = BODY_STREAMING_RADIUS - + (int) Math.ceil(STREAMING_HORIZONTAL_DRIFT_HALO_BLOCKS); - int minChunkX = ChunkUtil.chunkCoordinate(centerBlockX - horizontalRadius); - int maxChunkX = ChunkUtil.chunkCoordinate(centerBlockX + horizontalRadius); - int minBlockY = Math.max(0, (int) Math.floor(Math.min( - positionY - BODY_STREAMING_RADIUS, - STREAMING_FALL_ENVELOPE_MIN_Y))); - int maxBlockY = Math.min(ChunkUtil.HEIGHT_MINUS_1, - (int) Math.floor(positionY + BODY_STREAMING_RADIUS)); - int minChunkY = ChunkUtil.indexSection(minBlockY); - int maxChunkY = ChunkUtil.indexSection(maxBlockY); - int minChunkZ = ChunkUtil.chunkCoordinate(centerBlockZ - horizontalRadius); - int maxChunkZ = ChunkUtil.chunkCoordinate(centerBlockZ + horizontalRadius); - for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { - for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { - columns.add(ChunkUtil.indexChunk(chunkX, chunkZ)); - for (int chunkY = minChunkY; chunkY <= maxChunkY; chunkY++) { - sections.add(new ChunkSection(chunkX, chunkY, chunkZ)); - } - } - } - } - - private boolean areChunksReady(@Nonnull BenchmarkChunks chunks) { - ChunkStore chunkStore = world.getChunkStore(); - Store chunkComponentStore = chunkStore.getStore(); - for (long chunkIndex : chunks.columns()) { - if (!isChunkTicking(chunkStore, chunkComponentStore, chunkIndex)) { - return false; - } - } - for (ChunkSection section : chunks.sections()) { - if (!isChunkSectionReady(chunkStore, chunkComponentStore, section)) { - return false; - } - } - return true; - } - - private int requestChunks(@Nonnull BenchmarkChunks chunks) { - int requested = 0; - ChunkStore chunkStore = world.getChunkStore(); - Store chunkComponentStore = chunkStore.getStore(); - for (long chunkIndex : chunks.columns()) { - if (!isChunkTicking(chunkStore, chunkComponentStore, chunkIndex)) { - chunkStore.getChunkReferenceAsync(chunkIndex, TICKING_CHUNK_REQUEST_FLAGS); - requested++; - } - } - for (ChunkSection section : chunks.sections()) { - if (!isChunkSectionReady(chunkStore, chunkComponentStore, section)) { - chunkStore.getChunkSectionReferenceAsync(section.x(), - section.y(), - section.z(), - TICKING_CHUNK_REQUEST_FLAGS); - requested++; - } - } - return requested; - } - - private int retainChunks(@Nonnull BenchmarkChunks chunks) { - int retained = 0; - ChunkStore chunkStore = world.getChunkStore(); - Store chunkComponentStore = chunkStore.getStore(); - for (long chunkIndex : chunks.columns()) { - Ref chunkRef = chunkStore.getChunkReference(chunkIndex); - retained += retainChunkRef(chunkComponentStore, chunkRef); - } - return retained; - } - - private int retainChunkRef(@Nonnull Store chunkComponentStore, - @Nullable Ref chunkRef) { - if (chunkRef == null || !chunkRef.isValid()) { - return 0; - } - WorldChunk worldChunk = chunkComponentStore.getComponent(chunkRef, WORLD_CHUNK_TYPE); - if (worldChunk == null || !worldChunk.is(ChunkFlag.TICKING)) { - return 0; - } - worldChunk.addKeepLoaded(); - worldChunk.resetKeepAlive(); - worldChunk.resetActiveTimer(); - retainedChunks.add(worldChunk); - return 1; - } - - private void releaseRetainedChunks() { - for (WorldChunk worldChunk : retainedChunks) { - worldChunk.removeKeepLoaded(); - } - retainedChunks.clear(); - } - - private boolean isChunkTicking(@Nonnull ChunkStore chunkStore, - @Nonnull Store chunkComponentStore, - long chunkIndex) { - Ref chunkRef = chunkStore.getChunkReference(chunkIndex); - if (chunkRef == null || !chunkRef.isValid()) { - return false; - } - WorldChunk worldChunk = chunkComponentStore.getComponent(chunkRef, WORLD_CHUNK_TYPE); - return worldChunk != null && worldChunk.is(ChunkFlag.TICKING); - } - - private boolean isChunkSectionReady(@Nonnull ChunkStore chunkStore, - @Nonnull Store chunkComponentStore, - @Nonnull ChunkSection section) { - Ref chunkRef = chunkStore.getChunkSectionReference(section.x(), - section.y(), - section.z()); - if (chunkRef == null || !chunkRef.isValid()) { - return false; - } - Archetype archetype = chunkComponentStore.getArchetype(chunkRef); - return !archetype.contains(ChunkStore.REGISTRY.getNonTickingComponentType()); - } - } - - private static CrucibleTestCase.TestOutcome outcome(@Nonnull List reports) { - List failed = reports.stream() - .filter(report -> report.health().status() == StageStatus.STOP) - .map(StageReport::summary) - .toList(); - if (!failed.isEmpty()) { - return CrucibleTestCase.TestOutcome.fail(String.join(" | ", failed)); - } - return CrucibleTestCase.TestOutcome.pass(); - } - - private static StageHealth assessHealth(int count, - double observedTickRate, - @Nonnull SpaceStats stats, - int missingChunks) { - List stops = new ArrayList<>(); - List warnings = new ArrayList<>(); - if (observedTickRate < configuredDouble(MIN_TPS_PROPERTY, 8.0)) { - stops.add("observedTPS=" + format(observedTickRate) + "<8"); - } - if (stats.worldCollisionBodies == 0) { - stops.add("worldCollisionBodies=0"); - } - if (stats.belowWorldMinBodies > 0) { - stops.add("belowWorldMinBodies=" + stats.belowWorldMinBodies); - } - if (stats.belowVoidBodies > 0) { - stops.add("belowVoidBodies=" + stats.belowVoidBodies); - } - int maxBelowGround = Math.max(5, (int) Math.ceil(count * 0.01)); - if (stats.belowTerrainBodies > maxBelowGround) { - stops.add("belowTerrainBodies=" + stats.belowTerrainBodies - + ">" + maxBelowGround); - } - if (stats.belowPlaneBodies > maxBelowGround) { - String planeReason = "belowPlaneBodies=" + stats.belowPlaneBodies - + ">" + maxBelowGround; - if (configuredBoolean(STRICT_PLANE_GATE_PROPERTY, false)) { - stops.add(planeReason); - } else { - warnings.add(planeReason + " (strictPlaneGate=false)"); - } - } - if (missingChunks > 0) { - stops.add("missingChunks=" + missingChunks); - } - if (!stops.isEmpty()) { - return new StageHealth(StageStatus.STOP, String.join("; ", stops)); - } - if (observedTickRate < configuredDouble(WARN_TPS_PROPERTY, 15.0)) { - warnings.add("observedTPS=" + format(observedTickRate) + "<15"); - } - if (!warnings.isEmpty()) { - return new StageHealth(StageStatus.WARN, String.join("; ", warnings)); - } - return new StageHealth(StageStatus.PASS, "within gates"); - } - - private static double averageMillis(long nanos, int samples) { - if (samples <= 0) { - return 0.0; - } - return nanos / 1_000_000.0 / samples; - } - - private static String format(double value) { - return String.format(Locale.ROOT, "%.3f", value); - } - - private static String formatOptional(double value) { - return Double.isFinite(value) ? format(value) : "n/a"; - } - - private static int configuredInt(String property, int fallback, int min, int max) { - String raw = System.getProperty(property); - if (raw == null || raw.isBlank()) { - return fallback; - } - try { - return Math.clamp(Integer.parseInt(raw.trim()), min, max); - } catch (NumberFormatException ignored) { - return fallback; - } - } - - private static double configuredDouble(String property, double fallback) { - String raw = System.getProperty(property); - if (raw == null || raw.isBlank()) { - return fallback; - } - try { - return Double.parseDouble(raw.trim()); - } catch (NumberFormatException ignored) { - return fallback; - } - } - - private static boolean configuredBoolean(String property, boolean fallback) { - String raw = System.getProperty(property); - if (raw == null || raw.isBlank()) { - return fallback; - } - return Boolean.parseBoolean(raw.trim()); - } - - private record StagePlan(@Nonnull List counts, - int warmupTicks, - int sampleTicks) { - - private static StagePlan fromSystemProperties() { - return new StagePlan(configuredCounts(), - configuredInt(WARMUP_TICKS_PROPERTY, - DEFAULT_WARMUP_TICKS, - MIN_WARMUP_TICKS, - MAX_WARMUP_TICKS), - configuredInt(SAMPLE_TICKS_PROPERTY, - DEFAULT_SAMPLE_TICKS, - MIN_SAMPLE_TICKS, - MAX_SAMPLE_TICKS)); - } - - private static List configuredCounts() { - String raw = System.getProperty(COUNTS_PROPERTY); - if (raw == null || raw.isBlank()) { - return List.of(DEFAULT_STAGE_COUNT); - } - List counts = new ArrayList<>(); - for (String token : raw.split(",")) { - try { - int count = Math.clamp(Integer.parseInt(token.trim()), - MIN_STAGE_COUNT, - MAX_STAGE_COUNT); - counts.add(count); - } catch (NumberFormatException ignored) { - } - } - return counts.isEmpty() ? List.of(DEFAULT_STAGE_COUNT) : List.copyOf(counts); - } - } - - private record StartedStage(boolean started, - @Nullable SpaceId spaceId, - @Nullable BenchmarkChunks chunks, - int retainedColumns, - @Nullable PrewarmStats prewarm, - @Nonnull String failureMessage) { - - private static StartedStage started(@Nonnull SpaceId spaceId, - @Nonnull BenchmarkChunks chunks, - int retainedColumns, - @Nonnull PrewarmStats prewarm) { - return new StartedStage(true, spaceId, chunks, retainedColumns, prewarm, ""); - } - - private static StartedStage failed(int count, @Nonnull String failureMessage) { - return new StartedStage(false, - null, - new BenchmarkChunks(new LongOpenHashSet(), Set.of()), - 0, - new PrewarmStats(0, 0, 0), - "count=" + count + " " + failureMessage); - } - } - - private record StageReport(int count, - double observedTickRate, - double avgStepMs, - double avgSnapshotMs, - double avgSyncMs, - double avgWorldMs, - double totalMs, - int retainedColumns, - int chunkRefs, - int prewarmTargets, - int prewarmSectionsBuilt, - int bodies, - int dynamicBodies, - int worldCollisionBodies, - int belowPlaneBodies, - int belowTerrainBodies, - int belowWorldMinBodies, - int belowVoidBodies, - int terrainBaselineBodies, - int missingTerrainBaselineBodies, - double minTerrainBottomClearance, - int worldCollisionSamples, - int ensureCalls, - int sectionRequests, - int sectionCacheHits, - int sectionsBuilt, - int missingChunks, - int missingBlockChunks, - int missingBlockSections, - int uniqueMissingSections, - int missingOutsideRetainedEnvelope, - int bodyTargets, - @Nonnull StageHealth health) { - - private static StageReport failedPreflight(int count, @Nonnull String reason) { - StageHealth health = new StageHealth(StageStatus.STOP, reason); - return new StageReport(count, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - Double.NaN, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - health); - } - - private String summary() { - return "count=" + count - + " health=" + health.status() - + " reason=" + health.reason() - + " tps=" + format(observedTickRate) - + " totalMs=" + format(totalMs) - + " step/snapshot/sync/worldMs=" + format(avgStepMs) - + "/" + format(avgSnapshotMs) - + "/" + format(avgSyncMs) - + "/" + format(avgWorldMs) - + " bodies dynamic/worldCollision=" + dynamicBodies - + "/" + worldCollisionBodies - + " belowPlane/terrain/worldMin/void=" + belowPlaneBodies - + "/" + belowTerrainBodies - + "/" + belowWorldMinBodies - + "/" + belowVoidBodies - + " terrainBaseline samples/missing/minClearance=" + terrainBaselineBodies - + "/" + missingTerrainBaselineBodies - + "/" + formatOptional(minTerrainBottomClearance) - + " chunks retained/refs=" + retainedColumns - + "/" + chunkRefs - + " prewarm targets/built=" + prewarmTargets - + "/" + prewarmSectionsBuilt - + " streamingFallMinY=" + format(STREAMING_FALL_ENVELOPE_MIN_Y) - + " streamingHorizontalHalo=" + format(STREAMING_HORIZONTAL_DRIFT_HALO_BLOCKS) - + " world samples/ensure/req/hit/build/miss/bodyTargets=" - + worldCollisionSamples - + "/" + ensureCalls - + "/" + sectionRequests - + "/" + sectionCacheHits - + "/" + sectionsBuilt - + "/" + missingChunks - + "/" + bodyTargets - + " missing blockChunk/blockSection/unique/outsideRetained=" - + missingBlockChunks - + "/" + missingBlockSections - + "/" + uniqueMissingSections - + "/" + missingOutsideRetainedEnvelope - + " totalBodies=" + bodies; - } - } - - private record StageHealth(@Nonnull StageStatus status, @Nonnull String reason) { - } - - private enum StageStatus { - PASS, - WARN, - STOP - } - - private record PrewarmStats(int sectionTargets, int sectionsBuilt, int colliderBodies) { - } - - private record BenchmarkChunks(@Nonnull LongSet columns, @Nonnull Set sections) { - - private int size() { - return columns.size() + sections.size(); - } - } - - private record ChunkSection(int x, int y, int z) { - } - - private record BenchmarkLayout(@Nonnull Vector3d origin, int side, double spacing) { - - private static BenchmarkLayout flatGrid(int count) { - int side = (int) Math.ceil(Math.sqrt(count)); - double half = (side - 1) * DETACHED_SPACING * 0.5; - return new BenchmarkLayout(new Vector3d( - ORIGIN.x - half, - ORIGIN.y, - ORIGIN.z - half), side, DETACHED_SPACING); - } - - private double positionX(int index) { - int x = index % side; - return origin.x + x * spacing; - } - - private double positionY() { - return origin.y; - } - - private double positionZ(int index) { - int z = index / side; - return origin.z + z * spacing; - } - } - - private static final class SpaceStats { - - private int bodies; - private int dynamicBodies; - private int worldCollisionBodies; - private int belowPlaneBodies; - private int belowTerrainBodies; - private int belowWorldMinBodies; - private int belowVoidBodies; - private int terrainBaselineBodies; - private int missingTerrainBaselineBodies; - private double minTerrainBottomClearance = Double.POSITIVE_INFINITY; - - private static SpaceStats collect(@Nonnull PhysicsWorldRuntimeResource physics, - @Nonnull SpaceId spaceId) { - BenchmarkSpaceStatsView view = physics.queryInternal(new BenchmarkSpaceStatsQuery(spaceId, - GROUND_Y, - BELOW_PLANE_TOLERANCE, - BODY_WORLD_MIN_Y, - BODY_VOID_Y, - true)) - .toCompletableFuture() - .join(); - SpaceStats stats = new SpaceStats(); - stats.bodies = view.bodies(); - stats.dynamicBodies = view.dynamicBodies(); - stats.worldCollisionBodies = view.worldCollisionBodies(); - stats.belowPlaneBodies = view.belowPlaneBodies(); - stats.belowTerrainBodies = view.belowTerrainBodies(); - stats.belowWorldMinBodies = view.belowWorldMinBodies(); - stats.belowVoidBodies = view.belowVoidBodies(); - stats.terrainBaselineBodies = view.terrainBaselineBodies(); - stats.missingTerrainBaselineBodies = view.missingTerrainBaselineBodies(); - stats.minTerrainBottomClearance = view.minTerrainBottomClearance(); - return stats; - } - - private double minTerrainBottomClearance() { - return terrainBaselineBodies > 0 ? minTerrainBottomClearance : Double.NaN; - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ImpulseLiveCrucibleTests.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ImpulseLiveCrucibleTests.java deleted file mode 100644 index 570fcbfc..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ImpulseLiveCrucibleTests.java +++ /dev/null @@ -1,188 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.crucible; - -import com.hypixel.hytale.component.AddReason; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Holder; -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.entity.entities.BlockEntity; -import com.hypixel.hytale.server.core.modules.entity.DespawnComponent; -import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; -import com.hypixel.hytale.server.core.modules.time.TimeResource; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.modules.control.ImpulseControllableComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent.AttachmentLifecycle; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent.TransformAuthority; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RigidBodyStateQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.view.RigidBodyStateView; -import java.util.Comparator; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import org.joml.Vector3d; - -/** - * Crucible suites that exercise entity-backed bodies through Hytale's live ECS. - */ -final class ImpulseLiveCrucibleTests { - - private static final String DEFAULT_BLOCK_TYPE = "Rock_Stone"; - private static final ComponentType TRANSFORM_TYPE = - TransformComponent.getComponentType(); - private static final ComponentType DESPAWN_TYPE = - DespawnComponent.getComponentType(); - private static final ComponentType ATTACHMENT_TYPE = - PhysicsBodyAttachmentComponent.getComponentType(); - - private ImpulseLiveCrucibleTests() { - } - - static void register(CrucibleBridge bridge, ClassLoader loader) - throws ReflectiveOperationException { - - bridge.registerSuite(loader, ecsLiveSuite()); - } - - private static CrucibleSuite ecsLiveSuite() { - return new CrucibleSuite( - "impulse:ecs_live", - "Impulse ECS Live", - "Verifies entity-backed bodies move through the real Hytale ECS tick path", - Set.of("live", "integration"), - List.of(CrucibleTestCase.async("entity body falls", - ImpulseLiveCrucibleTests::entityBodyFallsThroughEcs, - "Entity-backed body did not fall through the live ECS tick path"))); - } - - private static CompletionStage entityBodyFallsThroughEcs(CrucibleContext context) { - try { - World world = context.world(); - Store store = world.getEntityStore().getStore(); - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); - SpaceId spaceId = liveTestSpaceId(resource, world); - - Vector3d visualPosition = new Vector3d( - context.wx(0), - context.wy(20), - context.wz(0)); - resource.submitCommands(0L, - commands -> commands.setSpaceGravity(spaceId, 0f, -9.81f, 0f)) - .completionSummary() - .toCompletableFuture() - .join(); - RigidBodyKey bodyKey = RigidBodyKey.random(); - submitLiveBody(resource, spaceId, bodyKey, visualPosition); - - Ref ref = spawnLiveBlockBody(store, spaceId, bodyKey, visualPosition); - double startY = visualPosition.y; - - return context.waitApproxTicksOnWorld(40).thenApply(ignored -> bodyAndEntityMovedDown( - store, - resource, - ref, - bodyKey, - startY)); - } catch (ReflectiveOperationException e) { - return CompletableFuture.failedFuture(e); - } - } - - private static boolean bodyAndEntityMovedDown(Store store, - PhysicsWorldResource resource, - Ref ref, - RigidBodyKey bodyKey, - double startY) { - - if (!ref.isValid()) { - return false; - } - TransformComponent transform = store.getComponent(ref, TRANSFORM_TYPE); - if (transform == null) { - return false; - } - double transformY = transform.getPosition().y; - Optional state = resource.query(new RigidBodyStateQuery(bodyKey)) - .completion() - .toCompletableFuture() - .join(); - if (state.isEmpty()) { - return false; - } - float bodyY = state.get().pose().position().y; - return transformY < startY - 0.05 && bodyY < startY - 0.05f; - } - - private static SpaceId liveTestSpaceId(PhysicsWorldResource resource, World world) { - SpaceId existingSpaceId = resource.getSpaceIds() - .stream() - .min(Comparator.comparingInt(SpaceId::value)) - .orElse(null); - if (existingSpaceId != null) { - return existingSpaceId; - } - return resource.createSpace(CrucibleBackends.requireBackendId(), - world.getName(), - PhysicsSpaceSettings.defaults()); - } - - private static void submitLiveBody(PhysicsWorldResource resource, - SpaceId spaceId, - RigidBodyKey bodyKey, - Vector3d visualPosition) { - resource.submitCommands(0L, - 1, - commands -> commands.spawnBody(bodyKey, spawn -> spawn - .space(spaceId) - .shape(PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f)) - .mass(1.0f) - .type(PhysicsBodyType.DYNAMIC) - .position((float) visualPosition.x, - (float) visualPosition.y, - (float) visualPosition.z) - .settings(RigidBodySpawnSettings.defaults()) - .kind(PhysicsBodyKind.BODY) - .persistence(PhysicsBodyPersistenceMode.PERSISTENT))) - .firstRejected() - .toCompletableFuture() - .join() - .ifPresent(result -> { - throw new IllegalStateException("spawn live crucible body command " - + result.commandSequence() + " rejected: " + result.message()); - }); - } - - private static Ref spawnLiveBlockBody(Store store, - SpaceId spaceId, - RigidBodyKey bodyKey, - Vector3d visualPosition) { - - TimeResource time = store.getResource(TimeResource.getResourceType()); - Holder holder = BlockEntity.assembleDefaultBlockEntity( - time, - DEFAULT_BLOCK_TYPE, - new Vector3d(visualPosition)); - holder.removeComponent(DESPAWN_TYPE); - holder.addComponent(ATTACHMENT_TYPE, - new PhysicsBodyAttachmentComponent(bodyKey, - spaceId, - TransformAuthority.BODY, - AttachmentLifecycle.EXTERNAL_ENTITY)); - holder.addComponent(ImpulseControllableComponent.getComponentType(), - new ImpulseControllableComponent()); - - return store.addEntity(holder, AddReason.SPAWN); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ImpulseRapierBodyBenchmarkCrucibleTests.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ImpulseRapierBodyBenchmarkCrucibleTests.java deleted file mode 100644 index 0100c42f..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/ImpulseRapierBodyBenchmarkCrucibleTests.java +++ /dev/null @@ -1,801 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.crucible; - -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.RemoveReason; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.logger.HytaleLogger; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.modules.control.components.PhysicsControlSessionComponent; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource.StepSnapshot; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource.SyncSnapshot; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource.Snapshot; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsBackendExtensionId; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSolverSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepSchedulingMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionMode; -import dev.hytalemodding.impulse.core.internal.simulation.query.BenchmarkSpaceStatsQuery; -import dev.hytalemodding.impulse.core.internal.simulation.view.BenchmarkSpaceStatsView; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.CompletionStage; -import java.util.logging.Level; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3d; - -/** - * Benchmark-oriented Crucible scenario for Rapier detached body-only fixed substeps. - */ -final class ImpulseRapierBodyBenchmarkCrucibleTests { - - private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); - private static final BackendId RAPIER_BACKEND_ID = new BackendId("impulse:rapier"); - private static final PhysicsBackendExtensionId RAPIER_SOLVER_EXTENSION_ID = - new PhysicsBackendExtensionId("impulse:rapier_solver"); - private static final String RAPIER_INTERNAL_PGS_ITERATIONS = "internalPgsIterations"; - private static final String RAPIER_MIN_ISLAND_SIZE = "minIslandSize"; - private static final String COUNT_PROPERTY = "impulse.crucible.rapierBodyMatrix.count"; - private static final String SUBSTEPS_PROPERTY = "impulse.crucible.rapierBodyMatrix.substeps"; - private static final String WARMUP_TICKS_PROPERTY = - "impulse.crucible.rapierBodyMatrix.warmupTicks"; - private static final String SAMPLE_TICKS_PROPERTY = - "impulse.crucible.rapierBodyMatrix.sampleTicks"; - private static final String MIN_TPS_PROPERTY = "impulse.crucible.rapierBodyMatrix.minTps"; - private static final String WARN_TPS_PROPERTY = "impulse.crucible.rapierBodyMatrix.warnTps"; - - private static final int DEFAULT_COUNT = 5_000; - private static final int DEFAULT_WARMUP_TICKS = 60; - private static final int DEFAULT_SAMPLE_TICKS = 200; - private static final int MIN_COUNT = 1; - private static final int MAX_COUNT = 10_000; - private static final int MIN_WARMUP_TICKS = 1; - private static final int MAX_WARMUP_TICKS = 1_200; - private static final int MIN_SAMPLE_TICKS = 20; - private static final int MAX_SAMPLE_TICKS = 7_200; - private static final float TARGET_MAX_STEP_DT = 1.0f / 30.0f; - private static final float GROUND_Y = 122.0f; - private static final float BELOW_PLANE_TOLERANCE = 1.0f; - private static final float BODY_WORLD_MIN_Y = -32.0f; - private static final float BODY_VOID_Y = -128.0f; - private static final double DETACHED_SPACING = 1.5; - private static final Vector3d ORIGIN = new Vector3d(0.0, 128.0, 0.0); - private static final ComponentType ATTACHMENT_TYPE = - PhysicsBodyAttachmentComponent.getComponentType(); - - private ImpulseRapierBodyBenchmarkCrucibleTests() { - } - - static void register(CrucibleBridge bridge, ClassLoader loader) - throws ReflectiveOperationException { - - bridge.registerSuite(loader, benchmarkSuite()); - } - - private static CrucibleSuite benchmarkSuite() { - return new CrucibleSuite( - "impulse:rapier_body_fixed_substep_benchmark", - "Impulse Rapier Body Fixed-Substep Benchmark", - "Runs the 5000 detached body-only Rapier benchmark for fixed substep counts", - Set.of("benchmark", "rapier", "body", "fixed"), - List.of(CrucibleTestCase.asyncResult("5000 body-only fixed substep matrix", - ImpulseRapierBodyBenchmarkCrucibleTests::rapierBodyMatrix, - "Rapier body fixed-substep benchmark breached a health gate"))); - } - - private static CompletionStage rapierBodyMatrix( - CrucibleContext context) { - if (!rapierBackendAvailable()) { - return CompletableFuture.completedFuture(CrucibleTestCase.TestOutcome.fail( - "Rapier backend impulse:rapier is not registered; available=" - + availableBackendIds())); - } - try { - MatrixPlan plan = MatrixPlan.fromSystemProperties(); - MatrixRunner runner = new MatrixRunner(context, plan); - return runner.run(); - } catch (ReflectiveOperationException e) { - return CompletableFuture.failedFuture(e); - } - } - - private static boolean rapierBackendAvailable() { - return Impulse.getRuntimeProviders().stream() - .anyMatch(provider -> RAPIER_BACKEND_ID.equals(provider.getId())); - } - - @Nonnull - private static List availableBackendIds() { - return Impulse.getRuntimeProviders().stream() - .map(provider -> provider.getId().value()) - .sorted() - .toList(); - } - - private static final class MatrixRunner { - - private final CrucibleContext context; - private final MatrixPlan plan; - private final World world; - private final Store store; - private final PhysicsWorldRuntimeResource physics; - private final PhysicsRuntimeProfilingResource runtimeProfiling; - private final WorldCollisionProfilingResource worldCollisionProfiling; - private final PhysicsWorldSettings previousWorldSettings; - private final boolean previousRuntimeProfilingEnabled; - private final boolean previousWorldCollisionProfilingEnabled; - - private MatrixRunner(@Nonnull CrucibleContext context, @Nonnull MatrixPlan plan) - throws ReflectiveOperationException { - this.context = context; - this.plan = plan; - this.world = context.world(); - this.store = world.getEntityStore().getStore(); - this.physics = PhysicsWorldRuntimeResource.require(store); - this.runtimeProfiling = store.getResource(PhysicsRuntimeProfilingResource.getResourceType()); - this.worldCollisionProfiling = store.getResource( - WorldCollisionProfilingResource.getResourceType()); - this.previousWorldSettings = physics.getWorldSettings(); - this.previousRuntimeProfilingEnabled = runtimeProfiling.isEnabled(); - this.previousWorldCollisionProfilingEnabled = worldCollisionProfiling.isEnabled(); - } - - private CompletionStage run() { - return runCase(0, new ArrayList<>()).handle((outcome, failure) -> { - clearCaseState(); - restoreSettings(); - if (failure != null) { - if (failure instanceof CompletionException completionException) { - throw completionException; - } - throw new CompletionException(failure); - } - return outcome; - }); - } - - private CompletionStage runCase(int index, - @Nonnull List reports) { - if (index >= plan.substeps().size()) { - logComparison(reports); - return CompletableFuture.completedFuture(outcome(reports)); - } - - MatrixCase matrixCase = new MatrixCase(plan.count(), plan.substeps().get(index)); - return startCase(matrixCase) - .thenCompose(started -> contextWait(plan.warmupTicks()).thenCompose(_ -> { - runtimeProfiling.reset(); - worldCollisionProfiling.reset(); - runtimeProfiling.setEnabled(true); - worldCollisionProfiling.setEnabled(true); - long startedNanos = System.nanoTime(); - return contextWait(plan.sampleTicks()).thenApply( - _ -> finishCase(matrixCase, started, startedNanos)); - })) - .thenCompose(report -> { - reports.add(report); - LOGGER.at(Level.INFO).log("Crucible Rapier body matrix case: %s", - report.summary()); - clearCaseState(); - if (report.health().status() == MatrixStatus.STOP) { - return CompletableFuture.completedFuture(outcome(reports)); - } - return runCase(index + 1, reports); - }); - } - - private CompletionStage startCase(@Nonnull MatrixCase matrixCase) { - clearCaseState(); - PhysicsWorldSettings worldSettings = physics.getWorldSettings(); - worldSettings.setStepMode(PhysicsStepMode.FIXED); - worldSettings.setStepSchedulingMode(PhysicsStepSchedulingMode.DROP_PENDING_DT); - worldSettings.setSimulationSteps(matrixCase.fixedSubsteps()); - worldSettings.setMaxStepDt(TARGET_MAX_STEP_DT); - physics.setWorldSettings(worldSettings); - physics.clearSyntheticVisualInterests(); - - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - settings.getWorldCollisionSettings().setWorldCollisionMode(WorldCollisionMode.NONE); - settings.getSolverSettings().setSolverIterations(PhysicsSolverSettings.DEFAULT_SOLVER_ITERATIONS); - settings.getSolverSettings().setStabilizationIterations( - PhysicsSolverSettings.DEFAULT_STABILIZATION_ITERATIONS); - settings.getExtensionSettings().setInt(RAPIER_SOLVER_EXTENSION_ID, - RAPIER_INTERNAL_PGS_ITERATIONS, - 1); - settings.getExtensionSettings().setInt(RAPIER_SOLVER_EXTENSION_ID, - RAPIER_MIN_ISLAND_SIZE, - 128); - try { - SpaceId spaceId = physics.createSpace(RAPIER_BACKEND_ID, - world.getName(), - settings); - return populateBenchmarkSpace(spaceId, matrixCase); - } catch (RuntimeException exception) { - return CompletableFuture.completedFuture( - StartedCase.failed(exception.getMessage())); - } - } - - private CompletionStage populateBenchmarkSpace(@Nonnull SpaceId spaceId, - @Nonnull MatrixCase matrixCase) { - BenchmarkLayout layout = BenchmarkLayout.flatGrid(matrixCase.count()); - RigidBodySpawnSettings groundSettings = RigidBodySpawnSettings.defaults() - .withCollisionFilter(PhysicsCollisionFilters.TERRAIN, PhysicsCollisionFilters.ALL); - RigidBodySpawnSettings bodySettings = RigidBodySpawnSettings.of(0.45f, - 0.0f, - 0.02f, - 0.25f, - PhysicsCollisionFilters.DYNAMIC_BODY, - PhysicsCollisionFilters.TERRAIN | PhysicsCollisionFilters.DYNAMIC_BODY); - long bodyKeyRunId = RigidBodyKey.random().mostSignificantBits(); - return physics.submitCommands(Math.max(0L, world.getTick()), - 2, - commands -> { - commands.spawnBody(RigidBodyKey.random(), - body -> body.space(spaceId) - .plane(GROUND_Y) - .type(PhysicsBodyType.STATIC) - .settings(groundSettings) - .temporary() - .runtimeOnly()); - commands.spawnBodies(matrixCase.count(), - spaceId, - PhysicsShapeSpec.box(0.48f, 0.48f, 0.48f), - 1.0f, - PhysicsBodyType.DYNAMIC, - bodySettings, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY, - spawns -> { - for (int i = 0; i < matrixCase.count(); i++) { - spawns.body(bodyKeyRunId, - i + 1L, - (float) layout.positionX(i), - (float) layout.positionY(), - (float) layout.positionZ(i)); - } - }); - }) - .completionSummary() - .handle((completion, failure) -> { - if (failure != null) { - return StartedCase.failed(failure.getMessage()); - } - return completion.firstRejected() - .map(result -> StartedCase.failed("prepare command " - + result.commandSequence() - + " rejected: " - + result.message())) - .orElseGet(() -> StartedCase.started(spaceId)); - }); - } - - private MatrixReport finishCase(@Nonnull MatrixCase matrixCase, - @Nonnull StartedCase started, - long startedNanos) { - if (!started.started()) { - return MatrixReport.failedPreflight(matrixCase, started.failureMessage()); - } - SpaceId spaceId = started.spaceId(); - if (spaceId == null || !physics.hasSpace(spaceId)) { - return MatrixReport.failedPreflight(matrixCase, - "space disappeared during benchmark"); - } - - StepSnapshot step = runtimeProfiling.getCumulativeStep(); - SyncSnapshot sync = runtimeProfiling.getCumulativeSync(); - Snapshot worldCollision = worldCollisionProfiling.getCumulativeSnapshot(); - double elapsedSeconds = Math.max(0.001, - (System.nanoTime() - startedNanos) / 1_000_000_000.0); - double observedTickRate = step.getTickSamples() / elapsedSeconds; - SpaceStats stats = SpaceStats.collect(physics, spaceId); - double avgStepMs = averageMillis(step.getTickNanos(), step.getTickSamples()); - double avgSnapshotMs = averageMillis(step.getSnapshotNanos(), step.getTickSamples()); - double avgSyncMs = averageMillis(sync.getTickNanos(), sync.getTickSamples()); - double avgWorldMs = averageMillis(worldCollision.getTickNanos(), - worldCollision.getTickSamples()); - double totalMs = avgStepMs + avgSnapshotMs + avgSyncMs + avgWorldMs; - MatrixHealth health = assessHealth(matrixCase, - observedTickRate, - step, - worldCollision, - stats); - - return new MatrixReport(matrixCase, - observedTickRate, - avgStepMs, - avgSnapshotMs, - avgSyncMs, - avgWorldMs, - totalMs, - step.getTickSamples(), - step.getSubsteps(), - step.getBodySnapshots(), - step.getSpatialIndexCells(), - sync.getTickSamples(), - sync.getBodiesInspected(), - sync.getBodiesSynced(), - worldCollision.getTickSamples(), - worldCollision.getStreamingSpaces(), - worldCollision.getEnsureCalls(), - worldCollision.getSectionRequests(), - worldCollision.getSectionsBuilt(), - worldCollision.getBodyStreamingTargets(), - stats.bodies, - stats.dynamicBodies, - stats.detachedBodies, - stats.rawBodies, - stats.worldCollisionBodies, - stats.awakeDynamicBodies, - stats.sleepingDynamicBodies, - stats.belowPlaneBodies, - stats.belowWorldMinBodies, - stats.belowVoidBodies, - stats.minDynamicBodyY(), - stats.maxDynamicBodyY(), - health); - } - - private CompletionStage contextWait(int ticks) { - try { - return context.waitApproxTicksOnWorld(ticks); - } catch (ReflectiveOperationException e) { - return CompletableFuture.failedFuture(e); - } - } - - private void clearCaseState() { - removeBenchmarkEntities(); - physics.clearSyntheticVisualInterests(); - physics.clearBodies(); - physics.clearAllSpaces(world.getName()); - runtimeProfiling.reset(); - worldCollisionProfiling.reset(); - worldCollisionProfiling.clearDiagnosticRetainedSections(); - } - - private void restoreSettings() { - physics.setWorldSettings(previousWorldSettings); - runtimeProfiling.setEnabled(previousRuntimeProfilingEnabled); - worldCollisionProfiling.setEnabled(previousWorldCollisionProfilingEnabled); - } - - private void removeBenchmarkEntities() { - store.forEachEntityParallel(ATTACHMENT_TYPE, - (index, archetypeChunk, commandBuffer) -> commandBuffer.removeEntity( - archetypeChunk.getReferenceTo(index), - RemoveReason.REMOVE)); - ComponentType controlSessionType = - PhysicsControlSessionComponent.getComponentType(); - store.forEachEntityParallel(controlSessionType, - (index, archetypeChunk, commandBuffer) -> commandBuffer.removeComponent( - archetypeChunk.getReferenceTo(index), - controlSessionType)); - } - - } - - private static CrucibleTestCase.TestOutcome outcome(@Nonnull List reports) { - List failed = reports.stream() - .filter(report -> report.health().status() == MatrixStatus.STOP) - .map(MatrixReport::summary) - .toList(); - if (!failed.isEmpty()) { - return CrucibleTestCase.TestOutcome.fail(String.join(" | ", failed)); - } - return CrucibleTestCase.TestOutcome.pass(); - } - - private static MatrixHealth assessHealth(@Nonnull MatrixCase matrixCase, - double observedTickRate, - @Nonnull StepSnapshot step, - @Nonnull Snapshot worldCollision, - @Nonnull SpaceStats stats) { - List stops = new ArrayList<>(); - List warnings = new ArrayList<>(); - if (step.getTickSamples() <= 0) { - stops.add("stepSamples=0"); - } - if (worldCollision.getTickSamples() <= 0) { - stops.add("worldCollisionSamples=0"); - } - if (stats.dynamicBodies != matrixCase.count()) { - stops.add("dynamicBodies=" + stats.dynamicBodies + "!=" + matrixCase.count()); - } - if (stats.detachedBodies != matrixCase.count()) { - stops.add("detachedBodies=" + stats.detachedBodies + "!=" + matrixCase.count()); - } - int expectedSubsteps = step.getTickSamples() * matrixCase.fixedSubsteps(); - if (step.getTickSamples() > 0 && step.getSubsteps() != expectedSubsteps) { - stops.add("substeps=" + step.getSubsteps() + "!=" + expectedSubsteps); - } - int expectedSnapshots = step.getTickSamples() * matrixCase.count(); - if (step.getTickSamples() > 0 && step.getBodySnapshots() != expectedSnapshots) { - stops.add("bodySnapshots=" + step.getBodySnapshots() + "!=" + expectedSnapshots); - } - if (worldCollision.getStreamingSpaces() > 0) { - stops.add("worldStreamingSpaces=" + worldCollision.getStreamingSpaces()); - } - if (worldCollision.getEnsureCalls() > 0) { - stops.add("worldEnsureCalls=" + worldCollision.getEnsureCalls()); - } - if (worldCollision.getSectionsBuilt() > 0) { - stops.add("worldSectionsBuilt=" + worldCollision.getSectionsBuilt()); - } - if (worldCollision.getBodyStreamingTargets() > 0) { - stops.add("worldBodyTargets=" + worldCollision.getBodyStreamingTargets()); - } - if (stats.worldCollisionBodies > 0) { - stops.add("worldCollisionBodies=" + stats.worldCollisionBodies); - } - if (stats.belowWorldMinBodies > 0) { - stops.add("belowWorldMinBodies=" + stats.belowWorldMinBodies); - } - if (stats.belowVoidBodies > 0) { - stops.add("belowVoidBodies=" + stats.belowVoidBodies); - } - int maxBelowGround = Math.max(5, (int) Math.ceil(matrixCase.count() * 0.01)); - if (stats.belowPlaneBodies > maxBelowGround) { - stops.add("belowPlaneBodies=" + stats.belowPlaneBodies + ">" + maxBelowGround); - } - double minTps = configuredDouble(MIN_TPS_PROPERTY, 8.0); - if (observedTickRate < minTps) { - stops.add("observedTPS=" + format(observedTickRate) + "<" + format(minTps)); - } - if (!stops.isEmpty()) { - return new MatrixHealth(MatrixStatus.STOP, String.join("; ", stops)); - } - double warnTps = configuredDouble(WARN_TPS_PROPERTY, 15.0); - if (observedTickRate < warnTps) { - warnings.add("observedTPS=" + format(observedTickRate) + "<" + format(warnTps)); - } - if (!warnings.isEmpty()) { - return new MatrixHealth(MatrixStatus.WARN, String.join("; ", warnings)); - } - return new MatrixHealth(MatrixStatus.PASS, "within gates"); - } - - private static void logComparison(@Nonnull List reports) { - if (reports.size() < 2) { - return; - } - MatrixReport first = reports.get(0); - MatrixReport second = reports.get(1); - LOGGER.at(Level.INFO).log("Crucible Rapier body matrix comparison: %sx=%sms " - + "%sx=%sms stepRatio=%s snapshotRatio=%s totalRatio=%s worldCounters=%s/%s", - first.matrixCase().fixedSubsteps(), - format(first.avgStepMs()), - second.matrixCase().fixedSubsteps(), - format(second.avgStepMs()), - format(ratio(second.avgStepMs(), first.avgStepMs())), - format(ratio(second.avgSnapshotMs(), first.avgSnapshotMs())), - format(ratio(second.totalMs(), first.totalMs())), - first.worldCounterSummary(), - second.worldCounterSummary()); - } - - private static double ratio(double numerator, double denominator) { - return denominator > 0.0 ? numerator / denominator : Double.NaN; - } - - private static double averageMillis(long nanos, int samples) { - if (samples <= 0) { - return 0.0; - } - return nanos / 1_000_000.0 / samples; - } - - private static String format(double value) { - return String.format(Locale.ROOT, "%.3f", value); - } - - private static String formatOptional(double value) { - return Double.isFinite(value) ? format(value) : "n/a"; - } - - private static int configuredInt(String property, int fallback, int min, int max) { - String raw = System.getProperty(property); - if (raw == null || raw.isBlank()) { - return fallback; - } - try { - return Math.clamp(Integer.parseInt(raw.trim()), min, max); - } catch (NumberFormatException ignored) { - return fallback; - } - } - - private static double configuredDouble(String property, double fallback) { - String raw = System.getProperty(property); - if (raw == null || raw.isBlank()) { - return fallback; - } - try { - return Double.parseDouble(raw.trim()); - } catch (NumberFormatException ignored) { - return fallback; - } - } - - private record MatrixPlan(int count, - @Nonnull List substeps, - int warmupTicks, - int sampleTicks) { - - private static MatrixPlan fromSystemProperties() { - return new MatrixPlan(configuredInt(COUNT_PROPERTY, DEFAULT_COUNT, MIN_COUNT, MAX_COUNT), - configuredSubsteps(), - configuredInt(WARMUP_TICKS_PROPERTY, - DEFAULT_WARMUP_TICKS, - MIN_WARMUP_TICKS, - MAX_WARMUP_TICKS), - configuredInt(SAMPLE_TICKS_PROPERTY, - DEFAULT_SAMPLE_TICKS, - MIN_SAMPLE_TICKS, - MAX_SAMPLE_TICKS)); - } - - @Nonnull - private static List configuredSubsteps() { - String raw = System.getProperty(SUBSTEPS_PROPERTY); - if (raw == null || raw.isBlank()) { - return List.of(1, 2); - } - List substeps = new ArrayList<>(); - for (String token : raw.split(",")) { - try { - int value = Math.clamp(Integer.parseInt(token.trim()), - PhysicsWorldSettings.MIN_SIMULATION_STEPS, - PhysicsWorldSettings.MAX_SIMULATION_STEPS); - if (!substeps.contains(value)) { - substeps.add(value); - } - } catch (NumberFormatException ignored) { - } - } - return substeps.isEmpty() ? List.of(1, 2) : List.copyOf(substeps); - } - } - - private record MatrixCase(int count, int fixedSubsteps) { - } - - private record StartedCase(boolean started, - @Nullable SpaceId spaceId, - @Nonnull String failureMessage) { - - private static StartedCase started(@Nonnull SpaceId spaceId) { - return new StartedCase(true, spaceId, ""); - } - - private static StartedCase failed(@Nullable String failureMessage) { - return new StartedCase(false, - null, - failureMessage != null ? failureMessage : "unknown startup failure"); - } - } - - private record MatrixReport(@Nonnull MatrixCase matrixCase, - double observedTickRate, - double avgStepMs, - double avgSnapshotMs, - double avgSyncMs, - double avgWorldMs, - double totalMs, - int stepSamples, - int substeps, - int bodySnapshots, - int spatialIndexCells, - int syncSamples, - int syncInspected, - int syncSynced, - int worldSamples, - int worldStreamingSpaces, - int worldEnsureCalls, - int worldSectionRequests, - int worldSectionsBuilt, - int worldBodyTargets, - int bodies, - int dynamicBodies, - int detachedBodies, - int rawBodies, - int worldCollisionBodies, - int awakeDynamicBodies, - int sleepingDynamicBodies, - int belowPlaneBodies, - int belowWorldMinBodies, - int belowVoidBodies, - double minDynamicBodyY, - double maxDynamicBodyY, - @Nonnull MatrixHealth health) { - - private static MatrixReport failedPreflight(@Nonnull MatrixCase matrixCase, - @Nonnull String reason) { - MatrixHealth health = new MatrixHealth(MatrixStatus.STOP, reason); - return new MatrixReport(matrixCase, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - Double.NaN, - Double.NaN, - health); - } - - private String summary() { - return "count=" + matrixCase.count() - + " fixedSubsteps=" + matrixCase.fixedSubsteps() - + " health=" + health.status() - + " reason=" + health.reason() - + " tps=" + format(observedTickRate) - + " totalMs=" + format(totalMs) - + " step/snapshot/sync/worldMs=" + format(avgStepMs) - + "/" + format(avgSnapshotMs) - + "/" + format(avgSyncMs) - + "/" + format(avgWorldMs) - + " step samples/substeps/bodySnapshots/spatialCells=" + stepSamples - + "/" + substeps - + "/" + bodySnapshots - + "/" + spatialIndexCells - + " sync samples/inspected/synced=" + syncSamples - + "/" + syncInspected - + "/" + syncSynced - + " world samples/streaming/ensure/req/build/bodyTargets=" - + worldSamples - + "/" + worldStreamingSpaces - + "/" + worldEnsureCalls - + "/" + worldSectionRequests - + "/" + worldSectionsBuilt - + "/" + worldBodyTargets - + " bodies total/dynamic/detached/raw/worldCollision=" + bodies - + "/" + dynamicBodies - + "/" + detachedBodies - + "/" + rawBodies - + "/" + worldCollisionBodies - + " awake/sleeping=" + awakeDynamicBodies - + "/" + sleepingDynamicBodies - + " belowPlane/worldMin/void=" + belowPlaneBodies - + "/" + belowWorldMinBodies - + "/" + belowVoidBodies - + " yMin/yMax=" + formatOptional(minDynamicBodyY) - + "/" + formatOptional(maxDynamicBodyY); - } - - private String worldCounterSummary() { - return worldSamples - + "/" + worldStreamingSpaces - + "/" + worldEnsureCalls - + "/" + worldSectionsBuilt - + "/" + worldBodyTargets; - } - } - - private record MatrixHealth(@Nonnull MatrixStatus status, @Nonnull String reason) { - } - - private enum MatrixStatus { - PASS, - WARN, - STOP - } - - private record BenchmarkLayout(@Nonnull Vector3d origin, int side, double spacing) { - - private static BenchmarkLayout flatGrid(int count) { - int side = (int) Math.ceil(Math.sqrt(count)); - double half = (side - 1) * DETACHED_SPACING * 0.5; - return new BenchmarkLayout(new Vector3d( - ORIGIN.x - half, - ORIGIN.y, - ORIGIN.z - half), side, DETACHED_SPACING); - } - - private double positionX(int index) { - return origin.x + (index % side) * spacing; - } - - private double positionY() { - return origin.y; - } - - private double positionZ(int index) { - return origin.z + ((double) index / side) * spacing; - } - } - - private static final class SpaceStats { - - private int bodies; - private int dynamicBodies; - private int awakeDynamicBodies; - private int sleepingDynamicBodies; - private int detachedBodies; - private int rawBodies; - private int worldCollisionBodies; - private int belowPlaneBodies; - private int belowWorldMinBodies; - private int belowVoidBodies; - private double minDynamicBodyY = Double.POSITIVE_INFINITY; - private double maxDynamicBodyY = Double.NEGATIVE_INFINITY; - - private static SpaceStats collect(@Nonnull PhysicsWorldRuntimeResource physics, - @Nonnull SpaceId spaceId) { - BenchmarkSpaceStatsView view = physics.queryInternal(new BenchmarkSpaceStatsQuery(spaceId, - GROUND_Y, - BELOW_PLANE_TOLERANCE, - BODY_WORLD_MIN_Y, - BODY_VOID_Y, - false)) - .toCompletableFuture() - .join(); - SpaceStats stats = new SpaceStats(); - stats.bodies = view.bodies(); - stats.dynamicBodies = view.dynamicBodies(); - stats.awakeDynamicBodies = view.awakeDynamicBodies(); - stats.sleepingDynamicBodies = view.sleepingDynamicBodies(); - stats.detachedBodies = view.detachedBodies(); - stats.rawBodies = view.rawBodies(); - stats.worldCollisionBodies = view.worldCollisionBodies(); - stats.belowPlaneBodies = view.belowPlaneBodies(); - stats.belowWorldMinBodies = view.belowWorldMinBodies(); - stats.belowVoidBodies = view.belowVoidBodies(); - stats.minDynamicBodyY = view.minDynamicBodyY(); - stats.maxDynamicBodyY = view.maxDynamicBodyY(); - return stats; - } - - private double minDynamicBodyY() { - return Double.isFinite(minDynamicBodyY) ? minDynamicBodyY : Double.NaN; - } - - private double maxDynamicBodyY() { - return Double.isFinite(maxDynamicBodyY) ? maxDynamicBodyY : Double.NaN; - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/WorldCollisionSubPluginCrucibleSupport.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/WorldCollisionSubPluginCrucibleSupport.java deleted file mode 100644 index 59b186be..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/crucible/WorldCollisionSubPluginCrucibleSupport.java +++ /dev/null @@ -1,88 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.crucible; - -import com.hypixel.hytale.common.plugin.PluginIdentifier; -import com.hypixel.hytale.server.core.plugin.PluginManager; -import com.hypixel.hytale.server.core.plugin.PluginBase; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldCollisionLifecycle; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.TimeUnit; -import javax.annotation.Nonnull; - -/** - * Runtime-only helpers for exercising the world-collision subplugin through Hytale. - */ -final class WorldCollisionSubPluginCrucibleSupport { - - private static final PluginIdentifier PLUGIN_ID = - new PluginIdentifier("HytaleModding", "ImpulseWorldCollision"); - - private WorldCollisionSubPluginCrucibleSupport() { - } - - static boolean ensureLoaded() { - PluginManager pluginManager = PluginManager.get(); - if (pluginManager.getPlugin(PLUGIN_ID) != null && WorldCollisionLifecycle.isEnabled()) { - return true; - } - return pluginManager.load(PLUGIN_ID) && WorldCollisionLifecycle.isEnabled(); - } - - @Nonnull - static CompletionStage loadUnloadReloadSmokeAsync() { - return CompletableFuture.supplyAsync(WorldCollisionSubPluginCrucibleSupport::loadUnloadReloadSmoke) - .orTimeout(30L, TimeUnit.SECONDS) - .exceptionally(failure -> CrucibleTestCase.TestOutcome.fail( - "World collision subplugin lifecycle smoke failed: " + failure.getMessage())); - } - - private static CrucibleTestCase.TestOutcome loadUnloadReloadSmoke() { - PluginManager pluginManager = PluginManager.get(); - if (!pluginManager.getAvailablePlugins().containsKey(PLUGIN_ID) - && pluginManager.getPlugin(PLUGIN_ID) == null) { - return CrucibleTestCase.TestOutcome.fail( - "World collision subplugin is not available: " + PLUGIN_ID); - } - - if (!ensureLoaded()) { - return CrucibleTestCase.TestOutcome.fail( - "World collision subplugin load did not enable the lifecycle"); - } - if (!pluginManager.unload(PLUGIN_ID)) { - return CrucibleTestCase.TestOutcome.fail( - "World collision subplugin unload returned false"); - } - if (pluginManager.getPlugin(PLUGIN_ID) != null) { - return CrucibleTestCase.TestOutcome.fail( - "World collision subplugin remained loaded after unload"); - } - if (WorldCollisionLifecycle.isEnabled()) { - return CrucibleTestCase.TestOutcome.fail( - "World collision subplugin unload did not disable the lifecycle"); - } - boolean loadResult = pluginManager.load(PLUGIN_ID); - PluginBase loadedPlugin = pluginManager.getPlugin(PLUGIN_ID); - if (!loadResult || !WorldCollisionLifecycle.isEnabled()) { - return CrucibleTestCase.TestOutcome.fail( - "World collision subplugin reload load did not enable the lifecycle: " - + "loadResult=" + loadResult - + ", pluginState=" + stateOf(loadedPlugin) - + ", lifecycleEnabled=" + WorldCollisionLifecycle.isEnabled()); - } - boolean reloadResult = pluginManager.reload(PLUGIN_ID); - PluginBase reloadedPlugin = pluginManager.getPlugin(PLUGIN_ID); - if (!reloadResult || !WorldCollisionLifecycle.isEnabled()) { - return CrucibleTestCase.TestOutcome.fail( - "World collision subplugin reload did not leave the lifecycle enabled: " - + "reloadResult=" + reloadResult - + ", pluginState=" + stateOf(reloadedPlugin) - + ", lifecycleEnabled=" + WorldCollisionLifecycle.isEnabled()); - } - return CrucibleTestCase.TestOutcome.pass(); - } - - @Nonnull - private static String stateOf(PluginBase plugin) { - return plugin == null ? "missing" : plugin.getState().name(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/math/PhysicsVisualPoseMath.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/math/PhysicsVisualPoseMath.java index 539b54da..e0f61add 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/math/PhysicsVisualPoseMath.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/math/PhysicsVisualPoseMath.java @@ -7,7 +7,9 @@ import org.joml.Vector3f; /** - * Transform helpers for physics bodies with Hytale visual attachments. + * Various helpers to fix annoyances between origin convention of EntityStore entities and PhysicsStore ones + * + * TODO: this and related PhysicsDebugResource methods should be owned by the PhysicsEntity module */ public final class PhysicsVisualPoseMath { diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/math/UuidMath.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/math/UuidMath.java deleted file mode 100644 index ded7288f..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/math/UuidMath.java +++ /dev/null @@ -1,29 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.math; - -/** - * Bit helpers for UUID-compatible identifiers. - * - *

Impulse random physics keys use {@code ThreadLocalRandom} longs because they are non-secret - * identifiers. This keeps UUID-shaped v4 values without paying for {@code UUID.randomUUID()}'s - * cryptographically strong generator on runtime allocation paths.

- */ -public final class UuidMath { - - // UUID v4 stores the version in bits 12-15 of the most-significant long. - private static final long UUID_VERSION_MASK = 0xffffffffffff0fffL; - private static final long UUID_VERSION_4_BITS = 0x0000000000004000L; - // RFC 4122/IETF UUIDs store variant bits 10xx in the top bits of the least-significant long. - private static final long UUID_VARIANT_MASK = 0x3fffffffffffffffL; - private static final long UUID_IETF_VARIANT_BITS = 0x8000000000000000L; - - private UuidMath() { - } - - public static long version4MostSignificantBits(long randomBits) { - return (randomBits & UUID_VERSION_MASK) | UUID_VERSION_4_BITS; - } - - public static long ietfVariantLeastSignificantBits(long randomBits) { - return (randomBits & UUID_VARIANT_MASK) | UUID_IETF_VARIANT_BITS; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/ImpulseSubPluginRegistration.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/ImpulseSubPluginRegistration.java index 4e9733ef..f9b73acc 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/ImpulseSubPluginRegistration.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/ImpulseSubPluginRegistration.java @@ -13,6 +13,7 @@ import java.net.URL; import java.nio.file.Path; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; import java.util.logging.Level; import javax.annotation.Nonnull; @@ -54,12 +55,30 @@ public static void register(@Nonnull JavaPlugin parentPlugin) { static List prepareSubPluginManifests(@Nonnull PluginManifest parentManifest) { List prepared = new ArrayList<>(); for (PluginManifest subPluginManifest : parentManifest.getSubPlugins()) { - subPluginManifest.inherit(parentManifest); - prepared.add(subPluginManifest); + PluginManifest mutableSubPluginManifest = mutableCopy(subPluginManifest); + mutableSubPluginManifest.inherit(parentManifest); + prepared.add(mutableSubPluginManifest); } return prepared; } + @Nonnull + private static PluginManifest mutableCopy(@Nonnull PluginManifest manifest) { + return new PluginManifest(manifest.getGroup(), + manifest.getName(), + manifest.getVersion(), + manifest.getDescription(), + new ArrayList<>(manifest.getAuthors()), + manifest.getWebsite(), + manifest.getMain(), + manifest.getServerVersion(), + new LinkedHashMap<>(manifest.getDependencies()), + new LinkedHashMap<>(manifest.getOptionalDependencies()), + new LinkedHashMap<>(manifest.getLoadBefore()), + new ArrayList<>(manifest.getSubPlugins()), + manifest.isDisabledByDefault()); + } + private static boolean hasPendingPlugin(@Nonnull List pendingPlugins, @Nonnull PluginIdentifier pluginId) { for (PendingLoadPlugin pendingPlugin : pendingPlugins) { diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/ControlLifecycle.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/ControlLifecycle.java deleted file mode 100644 index 3ffba361..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/ControlLifecycle.java +++ /dev/null @@ -1,192 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.control; - -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.logger.HytaleLogger; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.modules.SubPluginLifecycleGate; -import dev.hytalemodding.impulse.core.internal.modules.control.components.PhysicsControlSessionComponent; -import dev.hytalemodding.impulse.core.internal.modules.control.systems.PhysicsControlSessionCleanup; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.plugin.modules.control.ImpulseControllableComponent; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Server-level lifecycle controlled by the Impulse control subplugin. - */ -public final class ControlLifecycle { - - private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); - private static final SubPluginLifecycleGate GATE = new SubPluginLifecycleGate( - "Impulse control is disabled. Enable HytaleModding:ImpulseControl to start control sessions."); - private static final Set RESOURCES = - Collections.newSetFromMap(new WeakHashMap<>()); - private static final Set> STORES = - Collections.newSetFromMap(new WeakHashMap<>()); - private static final long CLEANUP_TIMEOUT_SECONDS = 5L; - - static { - GATE.onDisable(ControlLifecycle::cleanupStores); - GATE.onDisable(ControlLifecycle::cleanupResources); - } - - private ControlLifecycle() { - } - - public static void enable() { - GATE.enable(); - } - - public static void disable() { - GATE.disable(); - } - - public static boolean isEnabled() { - return GATE.isEnabled(); - } - - public static long generation() { - return GATE.generation(); - } - - public static void requireEnabled() { - GATE.requireEnabled(); - } - - public static void registerResource(@Nonnull PhysicsWorldRuntimeResource resource) { - synchronized (RESOURCES) { - RESOURCES.add(resource); - } - } - - public static void registerStore(@Nonnull Store store) { - synchronized (STORES) { - STORES.add(store); - } - } - - private static void cleanupStores() { - ComponentType controllableType = - ImpulseControllableComponent.isComponentTypeRegistered() - ? ImpulseControllableComponent.getComponentType() - : null; - ComponentType sessionType = - PhysicsControlSessionComponent.isComponentTypeRegistered() - ? PhysicsControlSessionComponent.getComponentType() - : null; - if (controllableType == null && sessionType == null) { - return; - } - ArrayList> stores; - synchronized (STORES) { - stores = new ArrayList<>(STORES); - } - for (Store store : stores) { - try { - cleanupStore(store, controllableType, sessionType); - } catch (RuntimeException exception) { - LOGGER.at(Level.WARNING).log("Failed to clean Impulse control components: %s", - exception.getMessage()); - } - } - } - - private static void cleanupStore(@Nonnull Store store, - @Nullable ComponentType controllableType, - @Nullable ComponentType sessionType) { - World world = store.getExternalData().getWorld(); - if (world.isInThread()) { - cleanupStoreOnOwnerThread(store, controllableType, sessionType); - return; - } - if (!world.isStarted()) { - return; - } - - CompletableFuture cleanup = new CompletableFuture<>(); - try { - world.execute(() -> { - try { - cleanupStoreOnOwnerThread(store, controllableType, sessionType); - cleanup.complete(null); - } catch (Throwable throwable) { - cleanup.completeExceptionally(throwable); - } - }); - } catch (RuntimeException exception) { - if (isWorldTaskRejection(exception)) { - return; - } - throw exception; - } - cleanup.orTimeout(CLEANUP_TIMEOUT_SECONDS, TimeUnit.SECONDS).join(); - } - - private static boolean isWorldTaskRejection(@Nonnull RuntimeException exception) { - Throwable current = exception; - while (current != null) { - if (current instanceof IllegalThreadStateException) { - return true; - } - current = current.getCause(); - } - return false; - } - - private static void cleanupStoreOnOwnerThread(@Nonnull Store store, - @Nullable ComponentType controllableType, - @Nullable ComponentType sessionType) { - if (sessionType != null) { - PhysicsWorldRuntimeResource resource = PhysicsWorldRuntimeResource.require(store); - ArrayList sessions = new ArrayList<>(); - store.forEachEntityParallel(sessionType, - (index, archetypeChunk, _) -> { - PhysicsControlSessionComponent session = - archetypeChunk.getComponent(index, sessionType); - if (session != null) { - SessionCleanupTarget target = new SessionCleanupTarget( - archetypeChunk.getReferenceTo(index), - session); - synchronized (sessions) { - sessions.add(target); - } - } - }); - for (SessionCleanupTarget target : sessions) { - PhysicsControlSessionCleanup.cleanupAndWait(store, resource, target.session()); - store.removeComponent(target.ref(), sessionType); - } - } - if (controllableType != null) { - store.forEachEntityParallel(controllableType, - (index, archetypeChunk, commandBuffer) -> - commandBuffer.removeComponent(archetypeChunk.getReferenceTo(index), - controllableType)); - } - } - - private static void cleanupResources() { - ArrayList resources; - synchronized (RESOURCES) { - resources = new ArrayList<>(RESOURCES); - } - for (PhysicsWorldRuntimeResource resource : resources) { - resource.disableControlLifecycle(); - } - } - - private record SessionCleanupTarget( - @Nonnull Ref ref, - @Nonnull PhysicsControlSessionComponent session) { - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/PhysicsControlRuntimeState.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/PhysicsControlRuntimeState.java deleted file mode 100644 index 3e0ec1b8..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/PhysicsControlRuntimeState.java +++ /dev/null @@ -1,34 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.control; - -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -import java.util.Set; -import javax.annotation.Nonnull; - -/** - * Runtime-only state for bodies currently driven by an Impulse control session. - */ -public final class PhysicsControlRuntimeState { - - private final Set controlledBodies = new ObjectOpenHashSet<>(); - - public synchronized void markBodyControlled(@Nonnull RigidBodyKey bodyKey) { - controlledBodies.add(bodyKey); - } - - public synchronized void clearControlledBody(@Nonnull RigidBodyKey bodyKey) { - controlledBodies.remove(bodyKey); - } - - public synchronized boolean isBodyControlled(@Nonnull RigidBodyKey bodyKey) { - return controlledBodies.contains(bodyKey); - } - - public synchronized void clearBody(@Nonnull RigidBodyKey bodyKey) { - controlledBodies.remove(bodyKey); - } - - public synchronized void clear() { - controlledBodies.clear(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsControlSessionCleanup.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsControlSessionCleanup.java deleted file mode 100644 index ed980f05..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsControlSessionCleanup.java +++ /dev/null @@ -1,124 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.control.systems; - -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.modules.control.components.PhysicsControlSessionComponent; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandHandle; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3f; - -public final class PhysicsControlSessionCleanup { - - private PhysicsControlSessionCleanup() { - } - - public static void cleanup(@Nonnull Store store, - @Nonnull PhysicsControlSessionComponent session) { - cleanup(store, PhysicsWorldRuntimeResource.require(store), session, false); - } - - public static void cleanup(@Nonnull PhysicsWorldResource resource, - @Nonnull PhysicsControlSessionComponent session) { - cleanup(PhysicsWorldRuntimeResource.require(resource), session); - } - - public static void cleanup(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull PhysicsControlSessionComponent session) { - cleanup(null, resource, session, false); - } - - public static void cleanupAndWait(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull PhysicsControlSessionComponent session) { - cleanup(null, resource, session, true); - } - - public static void cleanupAndWait(@Nonnull Store store, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull PhysicsControlSessionComponent session) { - cleanup(store, resource, session, true); - } - - private static void cleanup(@Nullable Store store, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull PhysicsControlSessionComponent session, - boolean waitForCompletion) { - if (store != null) { - PhysicsKinematicControlSystem.clearMutationState(store, session.getAnchorBodyKey()); - } - if (!session.isActive()) { - return; - } - - RigidBodyKey bodyKey = session.getBodyKey(); - RigidBodyKey anchorBodyKey = session.getAnchorBodyKey(); - JointKey controlJointKey = session.getControlJointKey(); - boolean restoreBody = bodyKey != null - && resource.hasPublishedOrPendingBodyRegistration(bodyKey); - if (bodyKey != null) { - resource.clearControlledBody(bodyKey); - } - - if (shouldReleaseJoint(controlJointKey, session.getSpaceId(), anchorBodyKey, bodyKey) - || restoreBody - || anchorBodyKey != null) { - if (waitForCompletion) { - resource.rejectSynchronousCompletionCallbackWait("cleanup control session"); - } - // Tick cleanup queues this work; subplugin shutdown waits for it. - PhysicsCommandHandle handle = - resource.submitCommands(0L, 5, commands -> { - if (session.getSpaceId() != null && bodyKey != null && anchorBodyKey != null) { - commands.destroyJointBetween(controlJointKey, session.getSpaceId(), anchorBodyKey, bodyKey); - } else if (controlJointKey != null) { - commands.destroyJoint(controlJointKey); - } - if (restoreBody) { - PhysicsBodyType originalBodyType = session.getOriginalBodyType(); - commands.setBodyType(bodyKey, originalBodyType); - if (originalBodyType == PhysicsBodyType.DYNAMIC) { - Vector3f releaseVelocity = session.getReleaseVelocity(); - commands.setBodyVelocity(bodyKey, - releaseVelocity.x, - releaseVelocity.y, - releaseVelocity.z, - 0.0f, - 0.0f, - 0.0f, - true); - } else { - commands.setBodyVelocity(bodyKey, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - true); - } - } - if (anchorBodyKey != null) { - commands.destroyBody(anchorBodyKey); - } - }); - if (waitForCompletion) { - handle.completionSummary().toCompletableFuture().join(); - } - } - session.deactivate(); - } - - private static boolean shouldReleaseJoint(@Nullable JointKey controlJointKey, - @Nullable SpaceId spaceId, - @Nullable RigidBodyKey anchorBodyKey, - @Nullable RigidBodyKey bodyKey) { - return (spaceId != null && anchorBodyKey != null && bodyKey != null) - || controlJointKey != null; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/ChunkCollisionMode.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/ChunkCollisionMode.java new file mode 100644 index 00000000..c9b3a01d --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/ChunkCollisionMode.java @@ -0,0 +1,17 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +/** + * Runtime representation used for full-cube chunk collision. + */ +public enum ChunkCollisionMode { + MERGED_BOXES, + NATIVE_VOXELS_WHEN_SUPPORTED; + + public static ChunkCollisionMode fromNativeVoxelCollisionEnabled(boolean enabled) { + return enabled ? NATIVE_VOXELS_WHEN_SUPPORTED : MERGED_BOXES; + } + + public boolean nativeVoxelCollisionEnabled() { + return this == NATIVE_VOXELS_WHEN_SUPPORTED; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/ChunkCollisionMutation.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/ChunkCollisionMutation.java new file mode 100644 index 00000000..2e79e1f9 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/ChunkCollisionMutation.java @@ -0,0 +1,95 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Copied chunk collision mutation emitted from PhysicsChunk collision code. + */ +public record ChunkCollisionMutation(@Nonnull UUID spaceUuid, + @Nonnull String sourceKey, + int chunkX, + int sectionY, + int chunkZ, + @Nonnull String payloadResourceKey, + @Nullable ChunkCollisionPayload payload, + boolean remove, + long lifecycleGeneration, + long settingsGeneration) { + + private static final long UNSTAMPED_GENERATION = 0L; + + public ChunkCollisionMutation { + Objects.requireNonNull(spaceUuid, "spaceUuid"); + Objects.requireNonNull(sourceKey, "sourceKey"); + Objects.requireNonNull(payloadResourceKey, "payloadResourceKey"); + } + + @Nonnull + public UUID chunkCollisionUuid() { + return chunkCollisionUuid(spaceUuid, sourceKey); + } + + @Nonnull + public static ChunkCollisionMutation upsert(@Nonnull UUID spaceUuid, + @Nonnull String sourceKey, + int chunkX, + int sectionY, + int chunkZ, + @Nonnull String payloadResourceKey, + @Nonnull ChunkCollisionPayload payload) { + return new ChunkCollisionMutation(spaceUuid, + sourceKey, + chunkX, + sectionY, + chunkZ, + payloadResourceKey, + payload, + false, + UNSTAMPED_GENERATION, + UNSTAMPED_GENERATION); + } + + @Nonnull + public static ChunkCollisionMutation remove(@Nonnull UUID spaceUuid, + @Nonnull String sourceKey, + int chunkX, + int sectionY, + int chunkZ) { + return new ChunkCollisionMutation(spaceUuid, + sourceKey, + chunkX, + sectionY, + chunkZ, + "", + null, + true, + UNSTAMPED_GENERATION, + UNSTAMPED_GENERATION); + } + + @Nonnull + public ChunkCollisionMutation stamped(long lifecycleGeneration, + long settingsGeneration) { + return new ChunkCollisionMutation(spaceUuid, + sourceKey, + chunkX, + sectionY, + chunkZ, + payloadResourceKey, + payload, + remove, + lifecycleGeneration, + settingsGeneration); + } + + @Nonnull + public static UUID chunkCollisionUuid(@Nonnull UUID spaceUuid, + @Nonnull String sourceKey) { + String key = spaceUuid + "|" + sourceKey; + return UUID.nameUUIDFromBytes(key.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/ChunkCollisionPayload.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/ChunkCollisionPayload.java new file mode 100644 index 00000000..4091203e --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/ChunkCollisionPayload.java @@ -0,0 +1,62 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * Copied chunk collision payload carried across the ChunkStore to PhysicsStore boundary. + */ +public record ChunkCollisionPayload(float voxelSizeX, + float voxelSizeY, + float voxelSizeZ, + @Nonnull int[] voxelCoordinates, + @Nonnull List mergedFullCubeBoxes, + @Nonnull List detailBoxes, + boolean nativeVoxelCollisionEnabled, + @Nonnull List neighbors) { + + public ChunkCollisionPayload { + voxelCoordinates = Arrays.copyOf(voxelCoordinates, voxelCoordinates.length); + mergedFullCubeBoxes = List.copyOf(mergedFullCubeBoxes); + detailBoxes = List.copyOf(detailBoxes); + neighbors = List.copyOf(neighbors); + } + + @Nonnull + @Override + public int[] voxelCoordinates() { + return Arrays.copyOf(voxelCoordinates, voxelCoordinates.length); + } + + public boolean hasFullCubeVoxels() { + return voxelCoordinates.length > 0; + } + + public boolean isEmpty() { + return voxelCoordinates.length == 0 && mergedFullCubeBoxes.isEmpty() + && detailBoxes.isEmpty(); + } + + /** + * Axis-aligned static terrain box in world coordinates. + */ + public record BoxPayload(double centerX, + double centerY, + double centerZ, + double halfX, + double halfY, + double halfZ) { + } + + /** + * Neighbor terrain source used for optional native-voxel stitching. + */ + public record Neighbor(@Nonnull String sourceKey, int shiftX, int shiftY, int shiftZ) { + + public Neighbor { + Objects.requireNonNull(sourceKey, "sourceKey"); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/ChunkSectionAccess.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/ChunkSectionAccess.java similarity index 94% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/ChunkSectionAccess.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/ChunkSectionAccess.java index 86551d0b..05da62b0 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/ChunkSectionAccess.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/ChunkSectionAccess.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision; +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkBuildOptions.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkBuildOptions.java new file mode 100644 index 00000000..1a721b9d --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkBuildOptions.java @@ -0,0 +1,33 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsChunkCollisionSettings; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * Options that control generated PhysicsChunk backend collision geometry. + */ +public record PhysicsChunkBuildOptions(@Nonnull ChunkCollisionMode chunkCollisionMode) { + + public static final PhysicsChunkBuildOptions DEFAULT = + fromNativeVoxelCollisionEnabled(PhysicsChunkCollisionSettings.DEFAULT_NATIVE_VOXEL_COLLISION_ENABLED); + + public PhysicsChunkBuildOptions { + Objects.requireNonNull(chunkCollisionMode, "chunkCollisionMode"); + } + + @Nonnull + public static PhysicsChunkBuildOptions fromSettings(@Nonnull PhysicsChunkCollisionSettings settings) { + return fromNativeVoxelCollisionEnabled(settings.isNativeVoxelCollisionEnabled()); + } + + @Nonnull + public static PhysicsChunkBuildOptions fromNativeVoxelCollisionEnabled(boolean enabled) { + return new PhysicsChunkBuildOptions( + ChunkCollisionMode.fromNativeVoxelCollisionEnabled(enabled)); + } + + public boolean nativeVoxelCollisionEnabled() { + return chunkCollisionMode.nativeVoxelCollisionEnabled(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkBuildStats.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkBuildStats.java new file mode 100644 index 00000000..e3729cdf --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkBuildStats.java @@ -0,0 +1,70 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import javax.annotation.Nonnull; + +/** + * Aggregate statistics from a PhysicsChunk collision build or rebuild operation. + */ +public record PhysicsChunkBuildStats(int scannedBlocks, + int solidBlocks, + int culledInteriorBlocks, + int fullCubeRuns, + int detailBoxes, + int colliderBodies, + int removedBodies, + int sectionsBuilt, + int sectionsRebuilt, + int voxelBodies) { + + @Nonnull + public static PhysicsChunkBuildStats empty() { + return new PhysicsChunkBuildStats(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + } + + @Nonnull + static PhysicsChunkBuildStats from(@Nonnull SectionCollisionGeometry geometry, + int colliderBodies, + int removedBodies, + int sectionsBuilt, + int sectionsRebuilt, + int voxelBodies) { + return new PhysicsChunkBuildStats(geometry.scannedBlocks(), + geometry.solidBlocks(), + geometry.culledInteriorBlocks(), + geometry.mergedFullCubeBoxes().size(), + geometry.detailBoxCount(), + colliderBodies, + removedBodies, + sectionsBuilt, + sectionsRebuilt, + voxelBodies); + } + + @Nonnull + public PhysicsChunkBuildStats plus(@Nonnull PhysicsChunkBuildStats stats) { + return new PhysicsChunkBuildStats(scannedBlocks + stats.scannedBlocks, + solidBlocks + stats.solidBlocks, + culledInteriorBlocks + stats.culledInteriorBlocks, + fullCubeRuns + stats.fullCubeRuns, + detailBoxes + stats.detailBoxes, + colliderBodies + stats.colliderBodies, + removedBodies + stats.removedBodies, + sectionsBuilt + stats.sectionsBuilt, + sectionsRebuilt + stats.sectionsRebuilt, + voxelBodies + stats.voxelBodies); + } + + @Nonnull + PhysicsChunkBuildStats withRemovedBodies(int removedBodies) { + return new PhysicsChunkBuildStats(scannedBlocks, + solidBlocks, + culledInteriorBlocks, + fullCubeRuns, + detailBoxes, + colliderBodies, + removedBodies, + sectionsBuilt, + sectionsRebuilt, + voxelBodies); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkCollisionDefaults.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkCollisionDefaults.java new file mode 100644 index 00000000..85275cd1 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkCollisionDefaults.java @@ -0,0 +1,17 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; + +/** + * Internal defaults for generated PhysicsChunk collision bodies. + */ +public final class PhysicsChunkCollisionDefaults { + + public static final float FRICTION = 0.75f; + public static final float RESTITUTION = 0.0f; + public static final int COLLISION_GROUP = PhysicsCollisionFilters.TERRAIN; + public static final int COLLISION_MASK = PhysicsCollisionFilters.ALL; + + private PhysicsChunkCollisionDefaults() { + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkCollisionStreamingResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkCollisionStreamingResource.java new file mode 100644 index 00000000..58bbe182 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkCollisionStreamingResource.java @@ -0,0 +1,285 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkMutationCache.TargetRefreshDecision; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.profiling.PhysicsChunkProfilingResource.Snapshot; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.profiling.PhysicsChunkProfilingResource.StreamingTargetDiagnostic; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionMutationQueueResource; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionBuildStats; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionPrewarmStats; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionStats; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3d; + +/** + * Shared EntityStore-side producer state for copied PhysicsStore chunk collision mutations. + */ +public final class PhysicsChunkCollisionStreamingResource implements Resource { + + @Nonnull + private final PhysicsChunkMutationCache cache; + private long tick; + + @Nullable + private static ResourceType resourceType; + + public PhysicsChunkCollisionStreamingResource() { + this(new PhysicsChunkMutationCache(), 0L); + } + + private PhysicsChunkCollisionStreamingResource(@Nonnull PhysicsChunkMutationCache cache, + long tick) { + this.cache = Objects.requireNonNull(cache, "cache"); + this.tick = tick; + } + + public static void setResourceType( + @Nonnull ResourceType type) { + resourceType = Objects.requireNonNull(type, "type"); + } + + public static void clearResourceType() { + resourceType = null; + } + + @Nonnull + public static ResourceType getResourceType() { + if (resourceType == null) { + throw new IllegalStateException("PhysicsStore PhysicsChunk collision streaming resource is not registered"); + } + return resourceType; + } + + public synchronized long nextTick() { + return ++tick; + } + + public synchronized void retainSpaces(@Nonnull Set retainedSpaces, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue) { + cache.retainSpaces(retainedSpaces, queue); + } + + @Nonnull + public synchronized PhysicsChunkCollisionPrewarmStats ensureAround(@Nonnull World world, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue, + @Nonnull Iterable centers, + int radius, + long tick, + @Nullable Snapshot profiling, + @Nonnull PhysicsChunkBuildOptions buildOptions) { + LongSet visitedSections = new LongOpenHashSet(); + PhysicsChunkBuildStats total = PhysicsChunkBuildStats.empty(); + PhysicsChunkSectionAccessCache accessCache = new PhysicsChunkSectionAccessCache(); + for (Vector3d center : centers) { + total = total.plus(ensureAround(world, + spaceUuid, + queue, + center, + radius, + tick, + profiling, + visitedSections, + null, + accessCache, + buildOptions)); + } + return new PhysicsChunkCollisionPrewarmStats(visitedSections.size(), collisionStats(total)); + } + + @Nonnull + public synchronized PhysicsChunkCollisionBuildStats refreshAround(@Nonnull World world, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue, + @Nonnull Vector3d center, + int radius, + long tick, + @Nullable Snapshot profiling, + @Nonnull PhysicsChunkBuildOptions buildOptions) { + int removed = cache.clearSectionsAround(spaceUuid, queue, center, radius); + PhysicsChunkSectionAccessCache accessCache = new PhysicsChunkSectionAccessCache(); + PhysicsChunkBuildStats stats = ensureAround(world, + spaceUuid, + queue, + center, + radius, + tick, + profiling, + null, + null, + accessCache, + buildOptions); + return collisionStats(withRemovedBodies(stats, stats.removedBodies() + removed)); + } + + @Nonnull + public synchronized PhysicsChunkBuildStats ensureAround(@Nonnull World world, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue, + @Nonnull Vector3d center, + int radius, + long tick, + @Nullable Snapshot profiling, + @Nullable LongSet visitedSections, + @Nullable StreamingTargetDiagnostic targetDiagnostic, + @Nullable PhysicsChunkSectionAccessCache accessCache, + @Nonnull PhysicsChunkBuildOptions buildOptions) { + return cache.ensureAround(world, + spaceUuid, + queue, + center, + radius, + tick, + profiling, + visitedSections, + targetDiagnostic, + accessCache, + buildOptions); + } + + @Nonnull + public synchronized TargetRefreshDecision shouldRefreshBodyTarget(@Nonnull UUID spaceUuid, + @Nonnull UUID bodyUuid, + @Nonnull PhysicsChunkStreamingBounds bounds, + boolean sleeping, + long currentTick, + int ttlTicks, + @Nullable Snapshot profiling) { + return cache.shouldRefreshBodyTarget(spaceUuid, + bodyUuid, + bounds, + sleeping, + currentTick, + ttlTicks, + profiling); + } + + @Nonnull + public synchronized TargetRefreshDecision shouldRefreshBodyTarget(@Nonnull UUID spaceUuid, + @Nonnull Ref bodyRef, + @Nonnull PhysicsChunkStreamingBounds bounds, + boolean sleeping, + long currentTick, + int ttlTicks, + @Nullable Snapshot profiling) { + return cache.shouldRefreshBodyTarget(spaceUuid, + bodyRef, + bounds, + sleeping, + currentTick, + ttlTicks, + profiling); + } + + public synchronized void recordBodyTargetRefresh(@Nonnull UUID spaceUuid, + @Nonnull UUID bodyUuid, + @Nonnull PhysicsChunkStreamingBounds bounds, + boolean sleeping, + long currentTick) { + cache.recordBodyTargetRefresh(spaceUuid, bodyUuid, bounds, sleeping, currentTick); + } + + public synchronized void recordBodyTargetRefresh(@Nonnull UUID spaceUuid, + @Nonnull Ref bodyRef, + @Nonnull PhysicsChunkStreamingBounds bounds, + boolean sleeping, + long currentTick) { + cache.recordBodyTargetRefresh(spaceUuid, bodyRef, bounds, sleeping, currentTick); + } + + public synchronized int pruneBodyStreamingTargets(@Nonnull UUID spaceUuid, + long currentTick, + int ttlTicks, + @Nullable Snapshot profiling) { + return cache.pruneBodyStreamingTargets(spaceUuid, currentTick, ttlTicks, profiling); + } + + public synchronized int pruneUnloaded(@Nonnull World world, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue, + @Nullable Snapshot profiling) { + return pruneUnloaded(world, + spaceUuid, + queue, + profiling, + new PhysicsChunkSectionAccessCache()); + } + + public synchronized int pruneUnloaded(@Nonnull World world, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue, + @Nullable Snapshot profiling, + @Nullable PhysicsChunkSectionAccessCache accessCache) { + return cache.pruneUnloaded(world, spaceUuid, queue, profiling, accessCache); + } + + public synchronized int pruneUnused(@Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue, + long currentTick, + int ttlTicks, + @Nullable Snapshot profiling) { + return cache.pruneUnused(spaceUuid, queue, currentTick, ttlTicks, profiling); + } + + public synchronized int clearSpace(@Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue) { + return cache.clearSpace(spaceUuid, queue); + } + + @Nonnull + public synchronized PhysicsChunkCollisionStats stats() { + return new PhysicsChunkCollisionStats(cache.spaceCount(), + cache.sectionCount(), + cache.bodyCount(), + cache.shapeTemplateCount()); + } + + public synchronized int bodyCount(@Nonnull UUID spaceUuid) { + return cache.bodyCount(spaceUuid); + } + + @Nonnull + @Override + public synchronized PhysicsChunkCollisionStreamingResource clone() { + return new PhysicsChunkCollisionStreamingResource(cache.copy(), tick); + } + + @Nonnull + private static PhysicsChunkCollisionBuildStats collisionStats(@Nonnull PhysicsChunkBuildStats stats) { + return new PhysicsChunkCollisionBuildStats(stats.scannedBlocks(), + stats.solidBlocks(), + stats.culledInteriorBlocks(), + stats.fullCubeRuns(), + stats.detailBoxes(), + stats.colliderBodies(), + stats.removedBodies(), + stats.sectionsBuilt(), + stats.sectionsRebuilt(), + stats.voxelBodies()); + } + + @Nonnull + private static PhysicsChunkBuildStats withRemovedBodies(@Nonnull PhysicsChunkBuildStats stats, int removedBodies) { + return new PhysicsChunkBuildStats(stats.scannedBlocks(), + stats.solidBlocks(), + stats.culledInteriorBlocks(), + stats.fullCubeRuns(), + stats.detailBoxes(), + stats.colliderBodies(), + removedBodies, + stats.sectionsBuilt(), + stats.sectionsRebuilt(), + stats.voxelBodies()); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkLifecycle.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkLifecycle.java new file mode 100644 index 00000000..ad3ea5fd --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkLifecycle.java @@ -0,0 +1,31 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import dev.hytalemodding.impulse.core.internal.modules.SubPluginLifecycleGate; + +/** + * Server-level lifecycle controlled by the Impulse PhysicsChunk subplugin. + */ +public final class PhysicsChunkLifecycle { + + private static final SubPluginLifecycleGate GATE = + new SubPluginLifecycleGate("Impulse PhysicsChunk subplugin is disabled"); + + private PhysicsChunkLifecycle() { + } + + public static void enable() { + GATE.enable(); + } + + public static void disable() { + GATE.disable(); + } + + public static boolean isEnabled() { + return GATE.isEnabled(); + } + + public static long generation() { + return GATE.generation(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkMutationCache.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkMutationCache.java new file mode 100644 index 00000000..7e51e94d --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkMutationCache.java @@ -0,0 +1,930 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.util.ChunkUtil; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.chunk.BlockChunk; +import com.hypixel.hytale.server.core.universe.world.chunk.section.BlockSection; +import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.profiling.PhysicsChunkProfilingResource.MissingSectionReason; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.profiling.PhysicsChunkProfilingResource.Snapshot; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.profiling.PhysicsChunkProfilingResource.StreamingTargetDiagnostic; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionMutationQueueResource; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2LongMap; +import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongSet; +import it.unimi.dsi.fastutil.objects.Object2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.util.Iterator; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3d; + +/** + * Section cache for PhysicsStore terrain mutation producers. + */ +public final class PhysicsChunkMutationCache { + + private static final int ACTIVE_BODY_STREAMING_INTERVAL_TICKS = 4; + private static final int SLEEPING_BODY_STREAMING_INTERVAL_TICKS = 20; + private static final int MISSING_BLOCK_CHUNK_RETRY_TICKS = 10; + private static final int MISSING_BLOCK_SECTION_RETRY_TICKS = 5; + private static final long BODY_TARGET_REFRESH_PENDING = Long.MIN_VALUE; + + @Nonnull + private final Object2ObjectMap spaces = + new Object2ObjectOpenHashMap<>(); + @Nonnull + private final ShapeTemplateCache shapeTemplates; + @Nonnull + private final SectionColliderBuilder sectionBuilder; + + public PhysicsChunkMutationCache() { + this(new ShapeTemplateCache()); + } + + private PhysicsChunkMutationCache(@Nonnull ShapeTemplateCache shapeTemplates) { + this.shapeTemplates = Objects.requireNonNull(shapeTemplates, "shapeTemplates"); + this.sectionBuilder = new SectionColliderBuilder(shapeTemplates); + } + + @Nonnull + synchronized PhysicsChunkMutationCache copy() { + PhysicsChunkMutationCache copy = new PhysicsChunkMutationCache(shapeTemplates.copy()); + for (Object2ObjectMap.Entry entry : spaces.object2ObjectEntrySet()) { + copy.spaces.put(entry.getKey(), entry.getValue().copy()); + } + return copy; + } + + @Nonnull + public synchronized PhysicsChunkBuildStats ensureAround(@Nonnull World world, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue, + @Nonnull Vector3d center, + int radius, + long tick, + @Nullable Snapshot profiling, + @Nullable LongSet visitedSections, + @Nullable StreamingTargetDiagnostic targetDiagnostic, + @Nullable PhysicsChunkSectionAccessCache accessCache, + @Nonnull PhysicsChunkBuildOptions buildOptions) { + long start = profiling != null ? System.nanoTime() : 0L; + if (profiling != null) { + profiling.incrementEnsureCalls(); + } + + int minX = (int) Math.floor(center.x) - radius; + int maxX = (int) Math.floor(center.x) + radius; + int minY = Math.max(0, (int) Math.floor(center.y) - radius); + int maxY = Math.min(ChunkUtil.HEIGHT_MINUS_1, (int) Math.floor(center.y) + radius); + int minZ = (int) Math.floor(center.z) - radius; + int maxZ = (int) Math.floor(center.z) + radius; + + int minChunkX = ChunkUtil.chunkCoordinate(minX); + int maxChunkX = ChunkUtil.chunkCoordinate(maxX); + int minSectionY = ChunkUtil.indexSection(minY); + int maxSectionY = ChunkUtil.indexSection(maxY); + int minChunkZ = ChunkUtil.chunkCoordinate(minZ); + int maxChunkZ = ChunkUtil.chunkCoordinate(maxZ); + + PhysicsChunkBuildStats total = PhysicsChunkBuildStats.empty(); + for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { + for (int sectionY = minSectionY; sectionY <= maxSectionY; sectionY++) { + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { + long key = packSectionKey(chunkX, sectionY, chunkZ); + if (visitedSections != null && !visitedSections.add(key)) { + if (profiling != null) { + profiling.incrementDuplicateSkips(); + } + continue; + } + total = total.plus(ensureSection(world, + spaceUuid, + queue, + chunkX, + sectionY, + chunkZ, + tick, + profiling, + targetDiagnostic, + accessCache, + buildOptions)); + } + } + } + if (profiling != null) { + profiling.addEnsureAroundNanos(System.nanoTime() - start); + } + return total; + } + + public synchronized int pruneUnused(@Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue, + long currentTick, + int ttlTicks, + @Nullable Snapshot profiling) { + long start = profiling != null ? System.nanoTime() : 0L; + SpaceCollisionCache cache = spaces.get(spaceUuid); + if (cache == null) { + return 0; + } + + int removedBodies = 0; + int removedSections = 0; + Iterator> iterator = + cache.sections.long2ObjectEntrySet().iterator(); + while (iterator.hasNext()) { + CachedSection section = iterator.next().getValue(); + if (currentTick - section.lastUsedTick <= ttlTicks) { + continue; + } + removedBodies += removeSection(spaceUuid, queue, section); + removedSections++; + iterator.remove(); + } + pruneExpiredMissingBackoffs(cache, currentTick); + if (cache.isEmpty()) { + spaces.remove(spaceUuid); + } + if (profiling != null) { + profiling.addTtlPrune(removedSections, removedBodies); + profiling.addPruneUnusedNanos(System.nanoTime() - start); + } + return removedBodies; + } + + public synchronized int pruneUnloaded(@Nonnull World world, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue, + @Nullable Snapshot profiling) { + return pruneUnloaded(world, spaceUuid, queue, profiling, null); + } + + public synchronized int pruneUnloaded(@Nonnull World world, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue, + @Nullable Snapshot profiling, + @Nullable PhysicsChunkSectionAccessCache accessCache) { + long start = profiling != null ? System.nanoTime() : 0L; + SpaceCollisionCache cache = spaces.get(spaceUuid); + if (cache == null) { + return 0; + } + + int removedBodies = 0; + int removedSections = 0; + Iterator> iterator = + cache.sections.long2ObjectEntrySet().iterator(); + while (iterator.hasNext()) { + CachedSection section = iterator.next().getValue(); + if (blockChunk(world, section.chunkX, section.chunkZ, accessCache) != null) { + continue; + } + removedBodies += removeSection(spaceUuid, queue, section); + removedSections++; + iterator.remove(); + } + if (cache.isEmpty()) { + spaces.remove(spaceUuid); + } + if (profiling != null) { + profiling.addUnloadedPrune(removedSections, removedBodies); + profiling.addPruneUnloadedNanos(System.nanoTime() - start); + } + return removedBodies; + } + + public synchronized void retainSpaces(@Nonnull Set retainedSpaces, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue) { + Iterator> iterator = + spaces.object2ObjectEntrySet().iterator(); + while (iterator.hasNext()) { + Object2ObjectMap.Entry entry = iterator.next(); + if (retainedSpaces.contains(entry.getKey())) { + continue; + } + removeAllSections(entry.getKey(), queue, entry.getValue()); + iterator.remove(); + } + } + + public synchronized int clearSpace(@Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue) { + SpaceCollisionCache cache = spaces.remove(spaceUuid); + if (cache == null) { + return 0; + } + return removeAllSections(spaceUuid, queue, cache); + } + + public synchronized int clearSectionsAround(@Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue, + @Nonnull Vector3d center, + int radius) { + SpaceCollisionCache cache = spaces.get(spaceUuid); + if (cache == null) { + return 0; + } + + int clampedRadius = Math.max(0, radius); + int minX = (int) Math.floor(center.x) - clampedRadius; + int maxX = (int) Math.floor(center.x) + clampedRadius; + int minY = Math.max(0, (int) Math.floor(center.y) - clampedRadius); + int maxY = Math.min(ChunkUtil.HEIGHT_MINUS_1, (int) Math.floor(center.y) + clampedRadius); + int minZ = (int) Math.floor(center.z) - clampedRadius; + int maxZ = (int) Math.floor(center.z) + clampedRadius; + + int minChunkX = ChunkUtil.chunkCoordinate(minX); + int maxChunkX = ChunkUtil.chunkCoordinate(maxX); + int minSectionY = ChunkUtil.indexSection(minY); + int maxSectionY = ChunkUtil.indexSection(maxY); + int minChunkZ = ChunkUtil.chunkCoordinate(minZ); + int maxChunkZ = ChunkUtil.chunkCoordinate(maxZ); + + int removed = 0; + Iterator> iterator = + cache.sections.long2ObjectEntrySet().iterator(); + while (iterator.hasNext()) { + CachedSection section = iterator.next().getValue(); + if (section.chunkX < minChunkX + || section.chunkX > maxChunkX + || section.sectionY < minSectionY + || section.sectionY > maxSectionY + || section.chunkZ < minChunkZ + || section.chunkZ > maxChunkZ) { + continue; + } + removed += removeSection(spaceUuid, queue, section); + iterator.remove(); + } + + for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { + for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { + cache.missingBlockChunkBackoffs.remove(ChunkUtil.indexChunk(chunkX, chunkZ)); + for (int sectionY = minSectionY; sectionY <= maxSectionY; sectionY++) { + cache.missingBlockSectionBackoffs.remove(packSectionKey(chunkX, + sectionY, + chunkZ)); + } + } + } + if (cache.isEmpty()) { + spaces.remove(spaceUuid); + } + return removed; + } + + @Nonnull + public synchronized TargetRefreshDecision shouldRefreshBodyTarget(@Nonnull UUID spaceUuid, + @Nonnull UUID bodyUuid, + @Nonnull PhysicsChunkStreamingBounds bounds, + boolean sleeping, + long currentTick, + int ttlTicks, + @Nullable Snapshot profiling) { + SpaceCollisionCache cache = spaces.computeIfAbsent(spaceUuid, _ -> new SpaceCollisionCache()); + CachedBodyStreamingTarget target = cache.bodyTargets.get(bodyUuid); + if (target == null) { + cache.bodyTargets.put(bodyUuid, new CachedBodyStreamingTarget(bounds, + sleeping, + currentTick, + BODY_TARGET_REFRESH_PENDING)); + if (profiling != null) { + profiling.incrementBodyTargetFirstSeen(); + } + return TargetRefreshDecision.refresh(TargetRefreshReason.FIRST_SEEN); + } + + target.lastSeenTick = currentTick; + target.sleeping = sleeping; + if (profiling != null) { + profiling.incrementBodyTargetCacheHits(); + } + + if (!target.bounds.equals(bounds)) { + target.bounds = bounds; + if (profiling != null) { + profiling.incrementBodyTargetBoundsChanged(); + } + return TargetRefreshDecision.refresh(TargetRefreshReason.BOUNDS_CHANGED); + } + if (target.lastRefreshTick == BODY_TARGET_REFRESH_PENDING) { + return TargetRefreshDecision.refresh(TargetRefreshReason.PENDING_APPLY); + } + + int interval = sleeping ? sleepingBodyStreamingInterval(ttlTicks) + : ACTIVE_BODY_STREAMING_INTERVAL_TICKS; + if (currentTick == 1L || currentTick - target.lastRefreshTick >= interval) { + if (profiling != null) { + if (sleeping) { + profiling.incrementBodyTargetSleepingRefreshes(); + } else { + profiling.incrementBodyTargetActiveRefreshes(); + } + } + return TargetRefreshDecision.refresh(sleeping + ? TargetRefreshReason.SLEEPING_INTERVAL + : TargetRefreshReason.ACTIVE_INTERVAL); + } + + if (profiling != null) { + if (sleeping) { + profiling.incrementBodyTargetSleepingStableSkips(); + } else { + profiling.incrementBodyTargetActiveStableSkips(); + } + } + return TargetRefreshDecision.skip(); + } + + @Nonnull + public synchronized TargetRefreshDecision shouldRefreshBodyTarget(@Nonnull UUID spaceUuid, + @Nonnull Ref bodyRef, + @Nonnull PhysicsChunkStreamingBounds bounds, + boolean sleeping, + long currentTick, + int ttlTicks, + @Nullable Snapshot profiling) { + SpaceCollisionCache cache = spaces.computeIfAbsent(spaceUuid, _ -> new SpaceCollisionCache()); + CachedBodyStreamingTarget target = getBodyTarget(cache, bodyRef); + if (target == null) { + putBodyTarget(cache, + bodyRef, + new CachedBodyStreamingTarget(bounds, + sleeping, + currentTick, + BODY_TARGET_REFRESH_PENDING)); + if (profiling != null) { + profiling.incrementBodyTargetFirstSeen(); + } + return TargetRefreshDecision.refresh(TargetRefreshReason.FIRST_SEEN); + } + + target.lastSeenTick = currentTick; + target.sleeping = sleeping; + if (profiling != null) { + profiling.incrementBodyTargetCacheHits(); + } + + if (!target.bounds.equals(bounds)) { + target.bounds = bounds; + if (profiling != null) { + profiling.incrementBodyTargetBoundsChanged(); + } + return TargetRefreshDecision.refresh(TargetRefreshReason.BOUNDS_CHANGED); + } + if (target.lastRefreshTick == BODY_TARGET_REFRESH_PENDING) { + return TargetRefreshDecision.refresh(TargetRefreshReason.PENDING_APPLY); + } + + int interval = sleeping ? sleepingBodyStreamingInterval(ttlTicks) + : ACTIVE_BODY_STREAMING_INTERVAL_TICKS; + if (currentTick == 1L || currentTick - target.lastRefreshTick >= interval) { + if (profiling != null) { + if (sleeping) { + profiling.incrementBodyTargetSleepingRefreshes(); + } else { + profiling.incrementBodyTargetActiveRefreshes(); + } + } + return TargetRefreshDecision.refresh(sleeping + ? TargetRefreshReason.SLEEPING_INTERVAL + : TargetRefreshReason.ACTIVE_INTERVAL); + } + + if (profiling != null) { + if (sleeping) { + profiling.incrementBodyTargetSleepingStableSkips(); + } else { + profiling.incrementBodyTargetActiveStableSkips(); + } + } + return TargetRefreshDecision.skip(); + } + + public synchronized void recordBodyTargetRefresh(@Nonnull UUID spaceUuid, + @Nonnull UUID bodyUuid, + @Nonnull PhysicsChunkStreamingBounds bounds, + boolean sleeping, + long currentTick) { + SpaceCollisionCache cache = spaces.computeIfAbsent(spaceUuid, _ -> new SpaceCollisionCache()); + CachedBodyStreamingTarget target = cache.bodyTargets.get(bodyUuid); + if (target == null) { + cache.bodyTargets.put(bodyUuid, new CachedBodyStreamingTarget(bounds, + sleeping, + currentTick, + currentTick)); + return; + } + target.bounds = bounds; + target.sleeping = sleeping; + target.lastSeenTick = currentTick; + target.lastRefreshTick = currentTick; + } + + public synchronized void recordBodyTargetRefresh(@Nonnull UUID spaceUuid, + @Nonnull Ref bodyRef, + @Nonnull PhysicsChunkStreamingBounds bounds, + boolean sleeping, + long currentTick) { + SpaceCollisionCache cache = spaces.computeIfAbsent(spaceUuid, _ -> new SpaceCollisionCache()); + CachedBodyStreamingTarget target = getBodyTarget(cache, bodyRef); + if (target == null) { + putBodyTarget(cache, + bodyRef, + new CachedBodyStreamingTarget(bounds, sleeping, currentTick, currentTick)); + return; + } + target.bounds = bounds; + target.sleeping = sleeping; + target.lastSeenTick = currentTick; + target.lastRefreshTick = currentTick; + } + + public synchronized int pruneBodyStreamingTargets(@Nonnull UUID spaceUuid, + long currentTick, + int ttlTicks, + @Nullable Snapshot profiling) { + SpaceCollisionCache cache = spaces.get(spaceUuid); + if (cache == null) { + return 0; + } + long maxAge = Math.max(1L, ttlTicks) * 2L; + int removed = 0; + Iterator> iterator = + cache.bodyTargets.object2ObjectEntrySet().iterator(); + while (iterator.hasNext()) { + CachedBodyStreamingTarget target = iterator.next().getValue(); + if (currentTick - target.lastSeenTick <= maxAge) { + continue; + } + iterator.remove(); + removed++; + } + Iterator> refIterator = + cache.bodyTargetsByRowIndex.int2ObjectEntrySet().iterator(); + while (refIterator.hasNext()) { + RefBodyStreamingTarget row = refIterator.next().getValue(); + if (row.bodyRef().isValid() + && currentTick - row.target().lastSeenTick <= maxAge) { + continue; + } + refIterator.remove(); + removed++; + } + pruneExpiredMissingBackoffs(cache, currentTick); + if (cache.isEmpty()) { + spaces.remove(spaceUuid); + } + if (removed > 0 && profiling != null) { + profiling.addBodyTargetsPruned(removed); + } + return removed; + } + + @Nonnull + private PhysicsChunkBuildStats ensureSection(@Nonnull World world, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue, + int chunkX, + int sectionY, + int chunkZ, + long tick, + @Nullable Snapshot profiling, + @Nullable StreamingTargetDiagnostic targetDiagnostic, + @Nullable PhysicsChunkSectionAccessCache accessCache, + @Nonnull PhysicsChunkBuildOptions buildOptions) { + long start = profiling != null ? System.nanoTime() : 0L; + if (profiling != null) { + profiling.incrementSectionRequests(); + } + + SpaceCollisionCache cache = spaces.computeIfAbsent(spaceUuid, _ -> new SpaceCollisionCache()); + long chunkKey = ChunkUtil.indexChunk(chunkX, chunkZ); + long sectionKey = packSectionKey(chunkX, sectionY, chunkZ); + if (isBackedOff(cache.missingBlockChunkBackoffs, chunkKey, tick)) { + recordMissingBackoff(profiling, + MissingSectionReason.BLOCK_CHUNK, + chunkX, + sectionY, + chunkZ, + targetDiagnostic, + start); + return PhysicsChunkBuildStats.empty(); + } + if (blockChunk(world, chunkX, chunkZ, accessCache) == null) { + cache.missingBlockChunkBackoffs.put(chunkKey, tick + MISSING_BLOCK_CHUNK_RETRY_TICKS); + recordMissing(profiling, + MissingSectionReason.BLOCK_CHUNK, + chunkX, + sectionY, + chunkZ, + targetDiagnostic, + start); + return PhysicsChunkBuildStats.empty(); + } + cache.missingBlockChunkBackoffs.remove(chunkKey); + + if (isBackedOff(cache.missingBlockSectionBackoffs, sectionKey, tick)) { + recordMissingBackoff(profiling, + MissingSectionReason.BLOCK_SECTION, + chunkX, + sectionY, + chunkZ, + targetDiagnostic, + start); + return PhysicsChunkBuildStats.empty(); + } + BlockSection section = accessCache != null + ? accessCache.blockSection(world, chunkX, sectionY, chunkZ) + : ChunkSectionAccess.blockSection(world, chunkX, sectionY, chunkZ); + if (section == null) { + cache.missingBlockSectionBackoffs.put(sectionKey, + tick + MISSING_BLOCK_SECTION_RETRY_TICKS); + recordMissing(profiling, + MissingSectionReason.BLOCK_SECTION, + chunkX, + sectionY, + chunkZ, + targetDiagnostic, + start); + return PhysicsChunkBuildStats.empty(); + } + cache.missingBlockSectionBackoffs.remove(sectionKey); + + long neighborhoodSignature = sectionBuilder.neighborhoodSignature(world, + section, + chunkX, + sectionY, + chunkZ, + accessCache); + CachedSection cached = cache.sections.get(sectionKey); + if (cached != null + && cached.neighborhoodSignature == neighborhoodSignature + && cached.buildOptions.equals(buildOptions)) { + cached.lastUsedTick = tick; + if (profiling != null) { + profiling.incrementSectionCacheHits(); + profiling.addEnsureSectionNanos(System.nanoTime() - start); + } + return PhysicsChunkBuildStats.empty(); + } + + SectionCollisionGeometry geometry = sectionBuilder.build(world, + section, + chunkX, + sectionY, + chunkZ, + accessCache); + CachedSection built = new CachedSection(chunkX, + sectionY, + chunkZ, + tick, + neighborhoodSignature, + buildOptions, + bodyCount(geometry, buildOptions), + buildOptions.nativeVoxelCollisionEnabled() && geometry.hasFullCubeVoxels()); + int removedBodies = cached != null ? removeSection(spaceUuid, queue, cached) : 0; + if (built.bodyCount > 0) { + queue.enqueue(PhysicsStoreChunkCollisionMutations.upsert(spaceUuid, + chunkX, + sectionY, + chunkZ, + neighborhoodSignature, + geometry, + buildOptions)); + } + cache.sections.put(sectionKey, built); + PhysicsChunkBuildStats stats = new PhysicsChunkBuildStats( + geometry.scannedBlocks(), + geometry.solidBlocks(), + geometry.culledInteriorBlocks(), + geometry.mergedFullCubeBoxes().size(), + geometry.detailBoxCount(), + built.bodyCount, + removedBodies, + cached == null ? 1 : 0, + cached == null ? 0 : 1, + built.voxelTerrain ? 1 : 0); + if (profiling != null) { + profiling.addBuildStats(stats); + profiling.addEnsureSectionNanos(System.nanoTime() - start); + } + return stats; + } + + private static int bodyCount(@Nonnull SectionCollisionGeometry geometry, + @Nonnull PhysicsChunkBuildOptions buildOptions) { + int fullCubeBodyCount = buildOptions.nativeVoxelCollisionEnabled() + && geometry.hasFullCubeVoxels() + ? 1 + : geometry.mergedFullCubeBoxes().size(); + return fullCubeBodyCount + geometry.detailBoxes().size(); + } + + private static int removeSection(@Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue, + @Nonnull CachedSection section) { + if (section.bodyCount <= 0) { + return 0; + } + queue.enqueue(PhysicsStoreChunkCollisionMutations.remove(spaceUuid, + section.chunkX, + section.sectionY, + section.chunkZ)); + return section.bodyCount; + } + + private static int removeAllSections(@Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue, + @Nonnull SpaceCollisionCache cache) { + int removed = 0; + for (CachedSection section : cache.sections.values()) { + removed += removeSection(spaceUuid, queue, section); + } + cache.sections.clear(); + return removed; + } + + public synchronized int bodyCount() { + int count = 0; + for (SpaceCollisionCache cache : spaces.values()) { + for (CachedSection section : cache.sections.values()) { + count += section.bodyCount; + } + } + return count; + } + + public synchronized int bodyCount(@Nonnull UUID spaceUuid) { + SpaceCollisionCache cache = spaces.get(spaceUuid); + if (cache == null) { + return 0; + } + int count = 0; + for (CachedSection section : cache.sections.values()) { + count += section.bodyCount; + } + return count; + } + + public synchronized int sectionCount() { + int count = 0; + for (SpaceCollisionCache cache : spaces.values()) { + count += cache.sections.size(); + } + return count; + } + + public synchronized int spaceCount() { + return spaces.size(); + } + + public synchronized int shapeTemplateCount() { + return shapeTemplates.size(); + } + + private static void recordMissingBackoff(@Nullable Snapshot profiling, + @Nonnull MissingSectionReason reason, + int chunkX, + int sectionY, + int chunkZ, + @Nullable StreamingTargetDiagnostic targetDiagnostic, + long start) { + if (profiling == null) { + return; + } + profiling.incrementMissingBackoffSkip(reason); + recordMissing(profiling, reason, chunkX, sectionY, chunkZ, targetDiagnostic, start); + } + + private static void recordMissing(@Nullable Snapshot profiling, + @Nonnull MissingSectionReason reason, + int chunkX, + int sectionY, + int chunkZ, + @Nullable StreamingTargetDiagnostic targetDiagnostic, + long start) { + if (profiling == null) { + return; + } + profiling.recordMissingSection(reason, chunkX, sectionY, chunkZ, targetDiagnostic); + profiling.addEnsureSectionNanos(System.nanoTime() - start); + } + + private static boolean isBackedOff(@Nonnull Long2LongMap backoffs, long key, long tick) { + return backoffs.containsKey(key) && backoffs.get(key) > tick; + } + + private static void pruneExpiredMissingBackoffs(@Nonnull SpaceCollisionCache cache, long tick) { + cache.missingBlockChunkBackoffs.long2LongEntrySet().removeIf(entry -> entry.getLongValue() <= tick); + cache.missingBlockSectionBackoffs.long2LongEntrySet().removeIf(entry -> entry.getLongValue() <= tick); + } + + private static int sleepingBodyStreamingInterval(int ttlTicks) { + int ttlBound = Math.max(1, ttlTicks / 4); + return Math.clamp(ttlBound, ACTIVE_BODY_STREAMING_INTERVAL_TICKS, + SLEEPING_BODY_STREAMING_INTERVAL_TICKS); + } + + @Nullable + private static CachedBodyStreamingTarget getBodyTarget(@Nonnull SpaceCollisionCache cache, + @Nonnull Ref bodyRef) { + RefBodyStreamingTarget row = cache.bodyTargetsByRowIndex.get(rowIndex(bodyRef)); + return row != null && sameRef(row.bodyRef(), bodyRef) ? row.target() : null; + } + + private static void putBodyTarget(@Nonnull SpaceCollisionCache cache, + @Nonnull Ref bodyRef, + @Nonnull CachedBodyStreamingTarget target) { + cache.bodyTargetsByRowIndex.put(rowIndex(bodyRef), + new RefBodyStreamingTarget(bodyRef, target)); + } + + private static int rowIndex(@Nonnull Ref bodyRef) { + return Objects.requireNonNull(bodyRef, "bodyRef").getIndex(); + } + + private static boolean sameRef(@Nonnull Ref first, + @Nonnull Ref second) { + return first.getIndex() == second.getIndex() + && first.getStore() == second.getStore(); + } + + private static long packSectionKey(int chunkX, int sectionY, int chunkZ) { + long x = ((long) chunkX & 0x3FFFFFL) << 42; + long y = ((long) sectionY & 0x3FFL) << 32; + long z = (long) chunkZ & 0xFFFFFFFFL; + return x | y | z; + } + + @Nullable + private static BlockChunk blockChunk(@Nonnull World world, int chunkX, int chunkZ) { + return blockChunk(world, chunkX, chunkZ, null); + } + + @Nullable + private static BlockChunk blockChunk(@Nonnull World world, + int chunkX, + int chunkZ, + @Nullable PhysicsChunkSectionAccessCache accessCache) { + if (accessCache != null) { + return accessCache.blockChunk(world, chunkX, chunkZ); + } + Ref chunkRef = world.getChunkStore() + .getChunkReference(ChunkUtil.indexChunk(chunkX, chunkZ)); + if (chunkRef == null || !chunkRef.isValid()) { + return null; + } + Store store = world.getChunkStore().getStore(); + return store.getComponentConcurrent(chunkRef, BlockChunk.getComponentType()); + } + + private static final class SpaceCollisionCache { + + private final Long2ObjectMap sections = new Long2ObjectOpenHashMap<>(); + private final Long2LongMap missingBlockChunkBackoffs = new Long2LongOpenHashMap(); + private final Long2LongMap missingBlockSectionBackoffs = new Long2LongOpenHashMap(); + private final Object2ObjectMap bodyTargets = + new Object2ObjectOpenHashMap<>(); + private final Int2ObjectOpenHashMap bodyTargetsByRowIndex = + new Int2ObjectOpenHashMap<>(); + + @Nonnull + private SpaceCollisionCache copy() { + SpaceCollisionCache copy = new SpaceCollisionCache(); + for (Long2ObjectMap.Entry entry : sections.long2ObjectEntrySet()) { + copy.sections.put(entry.getLongKey(), entry.getValue().copy()); + } + copy.missingBlockChunkBackoffs.putAll(missingBlockChunkBackoffs); + copy.missingBlockSectionBackoffs.putAll(missingBlockSectionBackoffs); + for (Object2ObjectMap.Entry entry + : bodyTargets.object2ObjectEntrySet()) { + copy.bodyTargets.put(entry.getKey(), entry.getValue().copy()); + } + for (Int2ObjectMap.Entry entry + : bodyTargetsByRowIndex.int2ObjectEntrySet()) { + RefBodyStreamingTarget row = entry.getValue(); + copy.bodyTargetsByRowIndex.put(entry.getIntKey(), + new RefBodyStreamingTarget(row.bodyRef(), row.target().copy())); + } + return copy; + } + + private boolean isEmpty() { + return sections.isEmpty() + && missingBlockChunkBackoffs.isEmpty() + && missingBlockSectionBackoffs.isEmpty() + && bodyTargets.isEmpty() + && bodyTargetsByRowIndex.isEmpty(); + } + } + + private static final class CachedSection { + + private final int chunkX; + private final int sectionY; + private final int chunkZ; + private final long neighborhoodSignature; + @Nonnull + private final PhysicsChunkBuildOptions buildOptions; + private final int bodyCount; + private final boolean voxelTerrain; + private long lastUsedTick; + + private CachedSection(int chunkX, + int sectionY, + int chunkZ, + long lastUsedTick, + long neighborhoodSignature, + @Nonnull PhysicsChunkBuildOptions buildOptions, + int bodyCount, + boolean voxelTerrain) { + this.chunkX = chunkX; + this.sectionY = sectionY; + this.chunkZ = chunkZ; + this.lastUsedTick = lastUsedTick; + this.neighborhoodSignature = neighborhoodSignature; + this.buildOptions = buildOptions; + this.bodyCount = bodyCount; + this.voxelTerrain = voxelTerrain; + } + + @Nonnull + private CachedSection copy() { + return new CachedSection(chunkX, + sectionY, + chunkZ, + lastUsedTick, + neighborhoodSignature, + buildOptions, + bodyCount, + voxelTerrain); + } + } + + private static final class CachedBodyStreamingTarget { + + @Nonnull + private PhysicsChunkStreamingBounds bounds; + private boolean sleeping; + private long lastSeenTick; + private long lastRefreshTick; + + private CachedBodyStreamingTarget(@Nonnull PhysicsChunkStreamingBounds bounds, + boolean sleeping, + long lastSeenTick, + long lastRefreshTick) { + this.bounds = bounds; + this.sleeping = sleeping; + this.lastSeenTick = lastSeenTick; + this.lastRefreshTick = lastRefreshTick; + } + + @Nonnull + private CachedBodyStreamingTarget copy() { + return new CachedBodyStreamingTarget(bounds, sleeping, lastSeenTick, lastRefreshTick); + } + } + + private record RefBodyStreamingTarget(@Nonnull Ref bodyRef, + @Nonnull CachedBodyStreamingTarget target) { + + private RefBodyStreamingTarget { + Objects.requireNonNull(bodyRef, "bodyRef"); + Objects.requireNonNull(target, "target"); + } + } + + public enum TargetRefreshReason { + FIRST_SEEN, + BOUNDS_CHANGED, + PENDING_APPLY, + ACTIVE_INTERVAL, + SLEEPING_INTERVAL, + STABLE_SKIP + } + + public record TargetRefreshDecision(boolean refresh, @Nonnull TargetRefreshReason reason) { + + @Nonnull + private static TargetRefreshDecision refresh(@Nonnull TargetRefreshReason reason) { + return new TargetRefreshDecision(true, reason); + } + + @Nonnull + private static TargetRefreshDecision skip() { + return new TargetRefreshDecision(false, TargetRefreshReason.STABLE_SKIP); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkSectionAccessCache.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkSectionAccessCache.java new file mode 100644 index 00000000..d5d837a1 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkSectionAccessCache.java @@ -0,0 +1,67 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.util.ChunkUtil; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.chunk.BlockChunk; +import com.hypixel.hytale.server.core.universe.world.chunk.section.BlockSection; +import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Per-streaming-pass source-section cache for repeated Hytale chunk lookups. + */ +public final class PhysicsChunkSectionAccessCache { + + private final Long2ObjectMap blockChunks = new Long2ObjectOpenHashMap<>(); + private final Long2ObjectMap blockSections = new Long2ObjectOpenHashMap<>(); + + public PhysicsChunkSectionAccessCache() { + } + + @Nullable + BlockChunk blockChunk(@Nonnull World world, int chunkX, int chunkZ) { + long key = ChunkUtil.indexChunk(chunkX, chunkZ); + if (blockChunks.containsKey(key)) { + return blockChunks.get(key); + } + + BlockChunk chunk = loadBlockChunk(world, chunkX, chunkZ); + blockChunks.put(key, chunk); + return chunk; + } + + @Nullable + BlockSection blockSection(@Nonnull World world, int chunkX, int sectionY, int chunkZ) { + long key = packSectionKey(chunkX, sectionY, chunkZ); + if (blockSections.containsKey(key)) { + return blockSections.get(key); + } + + BlockSection section = ChunkSectionAccess.blockSection(world, chunkX, sectionY, chunkZ); + blockSections.put(key, section); + return section; + } + + @Nullable + private static BlockChunk loadBlockChunk(@Nonnull World world, int chunkX, int chunkZ) { + Ref chunkRef = world.getChunkStore() + .getChunkReference(ChunkUtil.indexChunk(chunkX, chunkZ)); + if (chunkRef == null || !chunkRef.isValid()) { + return null; + } + Store store = world.getChunkStore().getStore(); + return store.getComponentConcurrent(chunkRef, BlockChunk.getComponentType()); + } + + private static long packSectionKey(int chunkX, int sectionY, int chunkZ) { + long x = ((long) chunkX & 0x3FFFFFL) << 42; + long y = ((long) sectionY & 0x3FFL) << 32; + long z = (long) chunkZ & 0xFFFFFFFFL; + return x | y | z; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkStoreTypes.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkStoreTypes.java new file mode 100644 index 00000000..5e367db4 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkStoreTypes.java @@ -0,0 +1,520 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.util.ChunkUtil; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionRestoreDependencyComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent.PartKind; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionMutationQueueResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionPayloadResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkComponentSyncResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkSettingsIndexResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkSettingsIndexResource.PhysicsChunkSpaceSettings; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems.ChunkCollisionComponentSyncSystem; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems.ChunkCollisionMutationDrainSystem; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems.ChunkCollisionRestorePrewarmSystem; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems.ChunkCollisionVoxelStitchingSystem; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems.PhysicsChunkSettingsIndexSystem; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsStoreCleanupHooks; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsStoreRowCleanup; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3d; +import org.joml.Vector3f; + +/** + * PhysicsStore-side type registration owned by the PhysicsChunk module. + */ +public final class PhysicsChunkStoreTypes { + + @Nonnull + private static final Consumer> FULL_STORE_CLEANUP = + PhysicsChunkStoreTypes::clearPhysicsStoreRuntimeResources; + @Nonnull + private static final Consumer> BODY_RUNTIME_CLEANUP = + PhysicsChunkStoreTypes::clearPhysicsStoreBodyRuntimeResources; + @Nonnull + private static final PhysicsStoreCleanupHooks.SpaceCleanup SPACE_CLEANUP = + PhysicsChunkStoreTypes::clearPhysicsStoreSpaceRuntimeResources; + @Nonnull + private static final PhysicsStoreCleanupHooks.BodyRowCleanup BODY_ROW_CLEANUP = + PhysicsChunkStoreTypes::clearPhysicsStoreBodyRowRuntimeResources; + + private PhysicsChunkStoreTypes() { + } + + public static void registerPhysicsStoreResourceTypes( + @Nonnull ComponentRegistryProxy registry) { + PhysicsChunkCollisionMutationQueueResource.setResourceType(registry.registerResource( + PhysicsChunkCollisionMutationQueueResource.class, + PhysicsChunkCollisionMutationQueueResource::new)); + PhysicsChunkCollisionPayloadResource.setResourceType(registry.registerResource( + PhysicsChunkCollisionPayloadResource.class, + PhysicsChunkCollisionPayloadResource::new)); + PhysicsChunkSettingsIndexResource.setResourceType(registry.registerResource( + PhysicsChunkSettingsIndexResource.class, + PhysicsChunkSettingsIndexResource::new)); + PhysicsChunkComponentSyncResource.setResourceType(registry.registerResource( + PhysicsChunkComponentSyncResource.class, + PhysicsChunkComponentSyncResource::new)); + } + + public static void clearPhysicsStoreResourceTypes() { + PhysicsChunkCollisionMutationQueueResource.clearResourceType(); + PhysicsChunkCollisionPayloadResource.clearResourceType(); + PhysicsChunkSettingsIndexResource.clearResourceType(); + PhysicsChunkComponentSyncResource.clearResourceType(); + } + + public static void registerPhysicsStoreCleanupHooks() { + PhysicsStoreCleanupHooks.registerFullStoreCleanup(FULL_STORE_CLEANUP); + PhysicsStoreCleanupHooks.registerBodyRuntimeCleanup(BODY_RUNTIME_CLEANUP); + PhysicsStoreCleanupHooks.registerSpaceCleanup(SPACE_CLEANUP); + PhysicsStoreCleanupHooks.registerBodyRowCleanup(BODY_ROW_CLEANUP); + } + + public static void clearPhysicsStoreCleanupHooks() { + PhysicsStoreCleanupHooks.unregisterFullStoreCleanup(FULL_STORE_CLEANUP); + PhysicsStoreCleanupHooks.unregisterBodyRuntimeCleanup(BODY_RUNTIME_CLEANUP); + PhysicsStoreCleanupHooks.unregisterSpaceCleanup(SPACE_CLEANUP); + PhysicsStoreCleanupHooks.unregisterBodyRowCleanup(BODY_ROW_CLEANUP); + } + + public static void registerSpaceBindingSystems( + @Nonnull ComponentRegistryProxy registry) { + registry.registerSystem(new PhysicsChunkSettingsIndexSystem()); + } + + public static void registerPreBodyBindingSystems( + @Nonnull ComponentRegistryProxy registry) { + registry.registerSystem(new ChunkCollisionMutationDrainSystem()); + registry.registerSystem(new ChunkCollisionRestorePrewarmSystem()); + } + + public static void registerPostBodyBindingSystems( + @Nonnull ComponentRegistryProxy registry) { + registry.registerSystem(new ChunkCollisionComponentSyncSystem()); + registry.registerSystem(new ChunkCollisionVoxelStitchingSystem()); + } + + public static void clearPhysicsStoreRuntimeResources(@Nonnull Store store) { + clearIfPresent(store, + PhysicsChunkCollisionMutationQueueResource.getResourceType(), + PhysicsChunkCollisionMutationQueueResource::clear); + clearIfPresent(store, + PhysicsChunkCollisionPayloadResource.getResourceType(), + PhysicsChunkCollisionPayloadResource::clear); + clearIfPresent(store, + PhysicsChunkSettingsIndexResource.getResourceType(), + PhysicsChunkSettingsIndexResource::clear); + clearIfPresent(store, + PhysicsChunkComponentSyncResource.getResourceType(), + PhysicsChunkComponentSyncResource::clear); + } + + private static void clearPhysicsStoreBodyRuntimeResources( + @Nonnull Store store) { + clearIfPresent(store, + PhysicsChunkCollisionMutationQueueResource.getResourceType(), + PhysicsChunkCollisionMutationQueueResource::clear); + clearIfPresent(store, + PhysicsChunkCollisionPayloadResource.getResourceType(), + PhysicsChunkCollisionPayloadResource::clear); + } + + private static void clearPhysicsStoreSpaceRuntimeResources( + @Nonnull Store store, + @Nonnull UUID spaceUuid) { + PhysicsChunkCollisionMutationQueueResource queue = + resourceIfPresent(store, PhysicsChunkCollisionMutationQueueResource.getResourceType()); + if (queue != null) { + queue.removeIf(mutation -> spaceUuid.equals(mutation.spaceUuid())); + } + } + + private static void clearPhysicsStoreBodyRowRuntimeResources( + @Nonnull Store store, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef) { + if (!bodyRef.isValid()) { + return; + } + ChunkCollisionSourceComponent source = store.getComponent(bodyRef, + ChunkCollisionSourceComponent.getComponentType()); + if (source == null) { + return; + } + removePayload(store, source.getPayloadResourceKey()); + } + + public static int clearChunkCollisionRowsForSpace(@Nonnull Store store, + @Nonnull UUID spaceUuid) { + PhysicsThreading.requireBackendIdle(store, "clear PhysicsStore chunk-collision rows"); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + Ref spaceRef = store.getExternalData().getRefFromUUID(spaceUuid); + List rows = collectChunkCollisionRows(store, spaceUuid, spaceRef); + rows.sort((first, second) -> Integer.compare(second.ref().getIndex(), + first.ref().getIndex())); + int removedBodies = 0; + List bodyRemovals = + new ArrayList<>(rows.size()); + for (GeneratedBodyRow row : rows) { + if (PhysicsStoreRowCleanup.removeRuntimeBody(store, runtime, row.uuid(), row.ref())) { + removedBodies++; + } + bodyRemovals.add(new PhysicsStoreRowCleanup.BodyEntityRemoval(row.uuid(), + row.ref())); + } + clearPhysicsStoreSpaceRuntimeResources(store, spaceUuid); + if (!bodyRemovals.isEmpty()) { + PhysicsStoreRowCleanup.removeBodyEntities(store, bodyRemovals); + PhysicsStoreRowCleanup.refreshIdentityAndRuntimeRefs(store); + } + return removedBodies; + } + + @Nullable + public static PhysicsChunkCollisionPayloadResource collisionPayloadsIfPresent( + @Nonnull Store store) { + return resourceIfPresent(store, PhysicsChunkCollisionPayloadResource.getResourceType()); + } + + public static void prewarmRestoreDependencies(@Nonnull Store store) { + if (!PhysicsChunkLifecycle.isEnabled()) { + return; + } + PhysicsChunkSettingsIndexResource settingsIndex = + resourceIfPresent(store, PhysicsChunkSettingsIndexResource.getResourceType()); + PhysicsChunkCollisionMutationQueueResource queue = + resourceIfPresent(store, PhysicsChunkCollisionMutationQueueResource.getResourceType()); + if (settingsIndex == null || queue == null) { + return; + } + World world = PhysicsThreading.world(store); + if (!world.isInThread()) { + return; + } + PhysicsChunkCollisionStreamingResource streaming = streamingIfPresent(world); + if (streaming == null) { + return; + } + + Map> centersByKey = + collectRestorePrewarmCenters(store, settingsIndex); + if (centersByKey.isEmpty()) { + return; + } + queue.updateStamp(PhysicsChunkLifecycle.generation(), settingsIndex.generation()); + long tick = Math.max(0L, world.getTick()); + for (Map.Entry> entry : centersByKey.entrySet()) { + RestorePrewarmKey key = entry.getKey(); + streaming.ensureAround(world, + key.settings().spaceUuid(), + queue, + entry.getValue(), + key.radius(), + tick, + null, + key.settings().buildOptions()); + } + } + + public static boolean shouldDeferChunkCollisionRestore(@Nonnull Store store, + @Nonnull CommandBuffer commandBuffer, + @Nonnull Ref bodyRef, + @Nonnull BodyComponent body, + @Nullable DynamicsComponent dynamics, + @Nonnull PhysicsRestoreStatusResource restore) { + ChunkCollisionRestoreDependencyComponent dependency = store.getComponent(bodyRef, + ChunkCollisionRestoreDependencyComponent.getComponentType()); + if (dependency == null) { + return false; + } + if (!dependency.getSpaceUuid().equals(body.getSpaceUuid())) { + clearRestoreDependency(commandBuffer, bodyRef); + return false; + } + if (dynamics == null || dynamics.getBodyType() != PhysicsBodyType.DYNAMIC) { + clearRestoreDependency(commandBuffer, bodyRef); + return false; + } + if (!PhysicsChunkLifecycle.isEnabled()) { + clearRestoreDependency(commandBuffer, bodyRef); + return false; + } + PhysicsChunkSettingsIndexResource settingsIndex = + resourceIfPresent(store, PhysicsChunkSettingsIndexResource.getResourceType()); + if (settingsIndex == null) { + clearRestoreDependency(commandBuffer, bodyRef); + return false; + } + PhysicsChunkSpaceSettings settings = settingsIndex.settings(body.getSpaceUuid()); + if (settings == null || settings.mode() == PhysicsChunkCollisionMode.NONE) { + clearRestoreDependency(commandBuffer, bodyRef); + return false; + } + prewarmRestoreDependency(store, settings, dependency); + if (hasGeneratedSupportRow(store, dependency)) { + clearRestoreDependency(commandBuffer, bodyRef); + return false; + } + restore.recordSoftSkip("Body restore dependency pending chunk collision"); + return true; + } + + private static > void clearIfPresent( + @Nonnull Store store, + @Nullable ResourceType type, + @Nonnull Consumer clear) { + T resource = resourceIfPresent(store, type); + if (resource != null) { + clear.accept(resource); + } + } + + @Nullable + private static > T resourceIfPresent( + @Nonnull Store store, + @Nullable ResourceType type) { + if (type == null) { + return null; + } + try { + type.validate(); + return store.getResource(type); + } catch (ArrayIndexOutOfBoundsException | IllegalArgumentException | IllegalStateException _) { + // PhysicsChunk resources are optional when the PhysicsChunk module is not registered. + return null; + } + } + + @Nonnull + private static List collectChunkCollisionRows( + @Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nullable Ref spaceRef) { + ComponentType uuidType = UuidComponent.getComponentType(); + ConcurrentLinkedQueue rows = new ConcurrentLinkedQueue<>(); + store.forEachEntityParallel(uuidType, (index, chunk, _) -> { + UuidComponent uuid = chunk.getComponent(index, uuidType); + if (uuid == null) { + return; + } + BodyComponent body = chunk.getComponent(index, BodyComponent.getComponentType()); + ChunkCollisionSourceComponent source = chunk.getComponent(index, + ChunkCollisionSourceComponent.getComponentType()); + if (source != null + && body != null + && matchesSpace(body.getSpaceRef(), body.getSpaceUuid(), spaceRef, spaceUuid)) { + rows.add(new GeneratedBodyRow(chunk.getReferenceTo(index), uuid.getUuid())); + } + }); + return new ArrayList<>(rows); + } + + private static boolean matchesSpace(@Nullable Ref rowSpaceRef, + @Nonnull UUID rowSpaceUuid, + @Nullable Ref spaceRef, + @Nonnull UUID spaceUuid) { + if (spaceRef != null && rowSpaceRef != null) { + return sameRef(rowSpaceRef, spaceRef); + } + return spaceUuid.equals(rowSpaceUuid); + } + + private static boolean sameRef(@Nonnull Ref first, + @Nonnull Ref second) { + return first.getStore() == second.getStore() + && first.getIndex() == second.getIndex(); + } + + private static void removePayload(@Nonnull Store store, + @Nullable String payloadResourceKey) { + if (payloadResourceKey == null || payloadResourceKey.isBlank()) { + return; + } + PhysicsChunkCollisionPayloadResource payloads = + resourceIfPresent(store, PhysicsChunkCollisionPayloadResource.getResourceType()); + if (payloads != null) { + payloads.remove(payloadResourceKey); + } + } + + @Nonnull + private static Map> collectRestorePrewarmCenters( + @Nonnull Store store, + @Nonnull PhysicsChunkSettingsIndexResource settingsIndex) { + Map> centersByKey = new Object2ObjectOpenHashMap<>(); + BiConsumer, CommandBuffer> collector = + (chunk, _) -> collectRestorePrewarmCenters(chunk, settingsIndex, centersByKey); + store.forEachChunk(UuidComponent.getComponentType(), collector); + return centersByKey; + } + + private static void collectRestorePrewarmCenters( + @Nonnull ArchetypeChunk chunk, + @Nonnull PhysicsChunkSettingsIndexResource settingsIndex, + @Nonnull Map> centersByKey) { + for (int index = 0; index < chunk.size(); index++) { + BodyComponent body = chunk.getComponent(index, BodyComponent.getComponentType()); + if (body == null) { + continue; + } + DynamicsComponent dynamics = chunk.getComponent(index, + DynamicsComponent.getComponentType()); + if (dynamics == null || dynamics.getBodyType() != PhysicsBodyType.DYNAMIC) { + continue; + } + ChunkCollisionRestoreDependencyComponent dependency = chunk.getComponent(index, + ChunkCollisionRestoreDependencyComponent.getComponentType()); + if (dependency == null) { + continue; + } + PhysicsChunkSpaceSettings settings = settingsIndex.settings(body.getSpaceUuid()); + if (settings == null || settings.mode() == PhysicsChunkCollisionMode.NONE) { + continue; + } + RestorePrewarmKey key = new RestorePrewarmKey(settings, dependency.getRadius()); + centersByKey.computeIfAbsent(key, _ -> new ArrayList<>()) + .add(vector3d(dependency.getCenter())); + } + } + + private static void prewarmRestoreDependency(@Nonnull Store store, + @Nonnull PhysicsChunkSpaceSettings settings, + @Nonnull ChunkCollisionRestoreDependencyComponent dependency) { + if (!PhysicsChunkLifecycle.isEnabled()) { + return; + } + World world = PhysicsThreading.world(store); + if (!world.isInThread()) { + return; + } + PhysicsChunkCollisionStreamingResource streaming = streamingIfPresent(world); + PhysicsChunkCollisionMutationQueueResource queue = + resourceIfPresent(store, PhysicsChunkCollisionMutationQueueResource.getResourceType()); + PhysicsChunkSettingsIndexResource settingsIndex = + resourceIfPresent(store, PhysicsChunkSettingsIndexResource.getResourceType()); + if (streaming == null || queue == null || settingsIndex == null) { + return; + } + queue.updateStamp(PhysicsChunkLifecycle.generation(), settingsIndex.generation()); + streaming.ensureAround(world, + settings.spaceUuid(), + queue, + List.of(vector3d(dependency.getCenter())), + dependency.getRadius(), + Math.max(0L, world.getTick()), + null, + settings.buildOptions()); + } + + @Nullable + private static PhysicsChunkCollisionStreamingResource streamingIfPresent(@Nonnull World world) { + try { + ResourceType type = + PhysicsChunkCollisionStreamingResource.getResourceType(); + return world.getEntityStore().getStore().getResource(type); + } catch (ArrayIndexOutOfBoundsException | IllegalArgumentException | IllegalStateException _) { + return null; + } + } + + private static boolean hasGeneratedSupportRow(@Nonnull Store store, + @Nonnull ChunkCollisionRestoreDependencyComponent dependency) { + Vector3f center = dependency.getCenter(); + int radius = dependency.getRadius(); + int minX = (int) Math.floor(center.x) - radius; + int maxX = (int) Math.floor(center.x) + radius; + int minY = Math.max(0, (int) Math.floor(center.y) - radius); + int maxY = Math.min(ChunkUtil.HEIGHT_MINUS_1, (int) Math.floor(center.y) + radius); + int minZ = (int) Math.floor(center.z) - radius; + int maxZ = (int) Math.floor(center.z) + radius; + + for (int chunkX = ChunkUtil.chunkCoordinate(minX); + chunkX <= ChunkUtil.chunkCoordinate(maxX); + chunkX++) { + for (int sectionY = ChunkUtil.indexSection(minY); + sectionY <= ChunkUtil.indexSection(maxY); + sectionY++) { + for (int chunkZ = ChunkUtil.chunkCoordinate(minZ); + chunkZ <= ChunkUtil.chunkCoordinate(maxZ); + chunkZ++) { + if (hasGeneratedSectionRow(store, dependency.getSpaceUuid(), chunkX, + sectionY, chunkZ)) { + return true; + } + } + } + } + return false; + } + + private static boolean hasGeneratedSectionRow(@Nonnull Store store, + @Nonnull UUID spaceUuid, + int chunkX, + int sectionY, + int chunkZ) { + String sourceKey = PhysicsStoreChunkCollisionMutations.sourceKey(chunkX, + sectionY, + chunkZ); + return hasGeneratedSectionPart(store, spaceUuid, sourceKey, PartKind.BOX) + || hasGeneratedSectionPart(store, spaceUuid, sourceKey, PartKind.DETAIL_BOX) + || hasGeneratedSectionPart(store, spaceUuid, sourceKey, PartKind.NATIVE_VOXELS); + } + + private static boolean hasGeneratedSectionPart(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull String sourceKey, + @Nonnull PartKind partKind) { + Ref ref = store.getExternalData().getRefFromUUID( + ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + sourceKey, + partKind, + 0)); + return ref != null && ref.isValid(); + } + + private static void clearRestoreDependency(@Nonnull CommandBuffer commandBuffer, + @Nonnull Ref bodyRef) { + commandBuffer.removeComponent(bodyRef, + ChunkCollisionRestoreDependencyComponent.getComponentType()); + } + + @Nonnull + private static Vector3d vector3d(@Nonnull Vector3f vector) { + return new Vector3d(vector.x, vector.y, vector.z); + } + + private record RestorePrewarmKey(@Nonnull PhysicsChunkSpaceSettings settings, int radius) { + } + + private record GeneratedBodyRow(@Nonnull Ref ref, @Nonnull UUID uuid) { + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldCollisionStreamingBounds.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkStreamingBounds.java similarity index 76% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldCollisionStreamingBounds.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkStreamingBounds.java index 6e4c2843..04583aa9 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldCollisionStreamingBounds.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkStreamingBounds.java @@ -1,16 +1,16 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision; +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; import com.hypixel.hytale.math.util.ChunkUtil; import javax.annotation.Nonnull; import org.joml.Vector3f; /** - * Chunk/section neighborhood covered by a streamed world-collision target. + * Chunk/section neighborhood covered by a streamed PhysicsChunk collision target. * *

Two body targets with the same bounds would trigger the same section * collision requests, so the streaming system can deduplicate them.

*/ -public record WorldCollisionStreamingBounds(int minChunkX, +public record PhysicsChunkStreamingBounds(int minChunkX, int maxChunkX, int minSectionY, int maxSectionY, @@ -18,12 +18,12 @@ public record WorldCollisionStreamingBounds(int minChunkX, int maxChunkZ) { @Nonnull - public static WorldCollisionStreamingBounds from(@Nonnull Vector3f center, int radius) { + public static PhysicsChunkStreamingBounds from(@Nonnull Vector3f center, int radius) { return from(center.x, center.y, center.z, radius); } @Nonnull - public static WorldCollisionStreamingBounds from(float centerX, + public static PhysicsChunkStreamingBounds from(float centerX, float centerY, float centerZ, int radius) { @@ -33,7 +33,7 @@ public static WorldCollisionStreamingBounds from(float centerX, int maxY = Math.clamp((int) Math.floor(centerY) + radius, 0, ChunkUtil.HEIGHT_MINUS_1); int minZ = (int) Math.floor(centerZ) - radius; int maxZ = (int) Math.floor(centerZ) + radius; - return new WorldCollisionStreamingBounds( + return new PhysicsChunkStreamingBounds( ChunkUtil.chunkCoordinate(minX), ChunkUtil.chunkCoordinate(maxX), ChunkUtil.indexSection(minY), diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkSubPlugin.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkSubPlugin.java new file mode 100644 index 00000000..15985c1a --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkSubPlugin.java @@ -0,0 +1,50 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.commands.PhysicsChunkCommandSet; +import dev.hytalemodding.impulse.core.internal.PhysicsStoreRegistration; +import java.util.logging.Level; +import javax.annotation.Nonnull; + +/** + * Bundled subplugin that enables Impulse PhysicsChunk collision integration. + */ +public final class PhysicsChunkSubPlugin extends JavaPlugin { + + private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); + + public PhysicsChunkSubPlugin(@Nonnull JavaPluginInit init) { + super(init); + } + + @Override + protected void setup() { + ComponentRegistryProxy entityRegistry = getEntityStoreRegistry(); + ComponentRegistryProxy physicsRegistry = + PhysicsStoreRegistration.physicsStoreRegistry(this); + PhysicsChunkTypes.registerEntityStoreResourceTypes(entityRegistry); + PhysicsChunkTypes.registerEntityStoreSystems(entityRegistry); + PhysicsChunkStoreTypes.registerPhysicsStoreResourceTypes(physicsRegistry); + PhysicsChunkStoreTypes.registerPhysicsStoreCleanupHooks(); + PhysicsChunkStoreTypes.registerSpaceBindingSystems(physicsRegistry); + PhysicsChunkStoreTypes.registerPreBodyBindingSystems(physicsRegistry); + PhysicsChunkStoreTypes.registerPostBodyBindingSystems(physicsRegistry); + PhysicsChunkCommandSet.register(); + PhysicsChunkLifecycle.enable(); + LOGGER.at(Level.INFO).log("Impulse PhysicsChunk collision producer enabled."); + } + + @Override + protected void shutdown() { + PhysicsChunkLifecycle.disable(); + PhysicsChunkCommandSet.unregister(); + PhysicsChunkStoreTypes.clearPhysicsStoreCleanupHooks(); + PhysicsChunkTypes.clearEntityStoreResourceTypes(); + PhysicsChunkStoreTypes.clearPhysicsStoreResourceTypes(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkTypes.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkTypes.java new file mode 100644 index 00000000..2af3c0b2 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkTypes.java @@ -0,0 +1,36 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.profiling.PhysicsChunkProfilingResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems.ChunkCollisionProducerSystem; +import javax.annotation.Nonnull; + +/** + * Registered EntityStore type handles owned by the PhysicsChunk subplugin. + */ +final class PhysicsChunkTypes { + + private PhysicsChunkTypes() { + } + + public static void registerEntityStoreResourceTypes( + @Nonnull ComponentRegistryProxy registry) { + PhysicsChunkProfilingResource.setResourceType(registry.registerResource( + PhysicsChunkProfilingResource.class, + PhysicsChunkProfilingResource::new)); + PhysicsChunkCollisionStreamingResource.setResourceType(registry.registerResource( + PhysicsChunkCollisionStreamingResource.class, + PhysicsChunkCollisionStreamingResource::new)); + } + + public static void registerEntityStoreSystems( + @Nonnull ComponentRegistryProxy registry) { + registry.registerSystem(new ChunkCollisionProducerSystem()); + } + + public static void clearEntityStoreResourceTypes() { + PhysicsChunkProfilingResource.clearResourceType(); + PhysicsChunkCollisionStreamingResource.clearResourceType(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsStoreChunkCollisionMutations.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsStoreChunkCollisionMutations.java new file mode 100644 index 00000000..eec7eb45 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsStoreChunkCollisionMutations.java @@ -0,0 +1,121 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.SectionCollisionGeometry.BoxCollider; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionMutation; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionPayload; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionPayload.BoxPayload; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionPayload.Neighbor; +import java.util.List; +import java.util.UUID; +import javax.annotation.Nonnull; + +/** + * Converts generated PhysicsChunk sections into copied PhysicsStore chunk collision mutations. + */ +public final class PhysicsStoreChunkCollisionMutations { + + private static final int ADJACENT_SECTION_VOXEL_SHIFT = 16; + + private PhysicsStoreChunkCollisionMutations() { + } + + @Nonnull + public static ChunkCollisionMutation upsert(@Nonnull UUID spaceUuid, + int chunkX, + int sectionY, + int chunkZ, + long neighborhoodSignature, + @Nonnull SectionCollisionGeometry geometry, + @Nonnull PhysicsChunkBuildOptions buildOptions) { + String sourceKey = sourceKey(chunkX, sectionY, chunkZ); + return ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + chunkX, + sectionY, + chunkZ, + payloadKey(sourceKey, neighborhoodSignature, buildOptions), + payload(geometry, buildOptions, adjacentNeighbors(chunkX, sectionY, chunkZ))); + } + + @Nonnull + public static ChunkCollisionMutation remove(@Nonnull UUID spaceUuid, + int chunkX, + int sectionY, + int chunkZ) { + return ChunkCollisionMutation.remove(spaceUuid, + sourceKey(chunkX, sectionY, chunkZ), + chunkX, + sectionY, + chunkZ); + } + + @Nonnull + public static String sourceKey(int chunkX, int sectionY, int chunkZ) { + return "chunk:" + chunkX + ":" + sectionY + ":" + chunkZ; + } + + @Nonnull + private static String payloadKey(@Nonnull String sourceKey, + long neighborhoodSignature, + @Nonnull PhysicsChunkBuildOptions buildOptions) { + return sourceKey + ":" + + Long.toUnsignedString(neighborhoodSignature) + + ":" + + Integer.toUnsignedString(buildOptions.hashCode()); + } + + @Nonnull + private static ChunkCollisionPayload payload(@Nonnull SectionCollisionGeometry geometry, + @Nonnull PhysicsChunkBuildOptions buildOptions, + @Nonnull List neighbors) { + return new ChunkCollisionPayload(1.0f, + 1.0f, + 1.0f, + geometry.fullCubeVoxels(), + boxes(geometry.mergedFullCubeBoxes()), + boxes(geometry.detailBoxes()), + buildOptions.nativeVoxelCollisionEnabled(), + neighbors); + } + + @Nonnull + private static List boxes(@Nonnull List boxes) { + return boxes.stream() + .map(box -> new BoxPayload(box.centerX(), + box.centerY(), + box.centerZ(), + box.halfX(), + box.halfY(), + box.halfZ())) + .toList(); + } + + @Nonnull + private static List adjacentNeighbors(int chunkX, int sectionY, int chunkZ) { + return List.of( + new Neighbor(sourceKey(chunkX - 1, sectionY, chunkZ), + -ADJACENT_SECTION_VOXEL_SHIFT, + 0, + 0), + new Neighbor(sourceKey(chunkX + 1, sectionY, chunkZ), + ADJACENT_SECTION_VOXEL_SHIFT, + 0, + 0), + new Neighbor(sourceKey(chunkX, sectionY - 1, chunkZ), + 0, + -ADJACENT_SECTION_VOXEL_SHIFT, + 0), + new Neighbor(sourceKey(chunkX, sectionY + 1, chunkZ), + 0, + ADJACENT_SECTION_VOXEL_SHIFT, + 0), + new Neighbor(sourceKey(chunkX, sectionY, chunkZ - 1), + 0, + 0, + -ADJACENT_SECTION_VOXEL_SHIFT), + new Neighbor(sourceKey(chunkX, sectionY, chunkZ + 1), + 0, + 0, + ADJACENT_SECTION_VOXEL_SHIFT)); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/SectionBlockReader.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/SectionBlockReader.java similarity index 94% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/SectionBlockReader.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/SectionBlockReader.java index 69a020de..956baf3a 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/SectionBlockReader.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/SectionBlockReader.java @@ -1,9 +1,9 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision; +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; import com.hypixel.hytale.math.util.ChunkUtil; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.chunk.section.BlockSection; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.ShapeTemplateCache.ShapeTemplate; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ShapeTemplateCache.ShapeTemplate; import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import javax.annotation.Nonnull; @@ -25,7 +25,7 @@ final class SectionBlockReader { private final int baseZ; private final Long2ObjectMap sectionCache = new Long2ObjectOpenHashMap<>(); @Nullable - private final WorldVoxelCollisionCache.SectionAccessCache accessCache; + private final PhysicsChunkSectionAccessCache accessCache; SectionBlockReader(@Nonnull World world, @Nonnull ShapeTemplateCache templates, @@ -42,7 +42,7 @@ final class SectionBlockReader { int currentChunkX, int currentSectionY, int currentChunkZ, - @Nullable WorldVoxelCollisionCache.SectionAccessCache accessCache) { + @Nullable PhysicsChunkSectionAccessCache accessCache) { this.world = world; this.templates = templates; this.currentSection = currentSection; diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/SectionColliderBuilder.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/SectionColliderBuilder.java similarity index 94% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/SectionColliderBuilder.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/SectionColliderBuilder.java index 234fa592..81a7ecc5 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/SectionColliderBuilder.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/SectionColliderBuilder.java @@ -1,11 +1,11 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision; +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; import com.hypixel.hytale.math.shape.Box; import com.hypixel.hytale.math.util.ChunkUtil; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.chunk.section.BlockSection; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.SectionCollisionGeometry.BoxCollider; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.ShapeTemplateCache.ShapeTemplate; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.SectionCollisionGeometry.BoxCollider; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ShapeTemplateCache.ShapeTemplate; import it.unimi.dsi.fastutil.ints.IntArrayList; import java.util.ArrayList; import java.util.BitSet; @@ -39,7 +39,7 @@ long neighborhoodSignature(@Nonnull World world, int chunkX, int sectionY, int chunkZ, - @Nullable WorldVoxelCollisionCache.SectionAccessCache accessCache) { + @Nullable PhysicsChunkSectionAccessCache accessCache) { return new SectionBlockReader(world, templates, section, chunkX, sectionY, chunkZ, accessCache) .neighborhoodSignature(); } @@ -59,7 +59,7 @@ SectionCollisionGeometry build(@Nonnull World world, int chunkX, int sectionY, int chunkZ, - @Nullable WorldVoxelCollisionCache.SectionAccessCache accessCache) { + @Nullable PhysicsChunkSectionAccessCache accessCache) { SectionBlockReader reader = new SectionBlockReader(world, templates, section, chunkX, sectionY, chunkZ, accessCache); BitSet fullCubes = new BitSet(SECTION_VOLUME); diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/SectionCollisionGeometry.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/SectionCollisionGeometry.java similarity index 96% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/SectionCollisionGeometry.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/SectionCollisionGeometry.java index da229ad3..9fae1232 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/SectionCollisionGeometry.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/SectionCollisionGeometry.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision; +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; import java.util.List; import javax.annotation.Nonnull; diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/ShapeTemplateCache.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/ShapeTemplateCache.java similarity index 92% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/ShapeTemplateCache.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/ShapeTemplateCache.java index 8e83a29d..bc3dd01c 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/ShapeTemplateCache.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/ShapeTemplateCache.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision; +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; import com.hypixel.hytale.math.shape.Box; import com.hypixel.hytale.protocol.BlockMaterial; @@ -36,6 +36,13 @@ int size() { return templates.size(); } + @Nonnull + ShapeTemplateCache copy() { + ShapeTemplateCache copy = new ShapeTemplateCache(); + copy.templates.putAll(templates); + return copy; + } + private static long key(int blockId, int rotation) { return ((long) blockId << Integer.SIZE) ^ (rotation & 0xFFFF_FFFFL); } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/CollisionLodSettingsCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/CollisionLodSettingsCommand.java similarity index 72% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/CollisionLodSettingsCommand.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/CollisionLodSettingsCommand.java index ca6ad0dc..7210248e 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/CollisionLodSettingsCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/CollisionLodSettingsCommand.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.commands; +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.commands; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; @@ -10,11 +10,11 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.commands.SpaceSelection; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsCollisionLodSettings; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsSpaces; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsCollisionLodSettings; import java.util.Locale; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; @@ -70,19 +70,26 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); - SpaceId spaceId = SpaceSelection.resolve(ctx, world, resource, spaceArg); - if (spaceId == null) { + Store physicsStore = PhysicsThreading.store(world); + PhysicsChunkSpaceSelection.Selection selection = + PhysicsChunkSpaceSelection.resolve(ctx, world, spaceArg, physicsStore); + if (selection == null) { return CompletableFuture.completedFuture(null); } - PhysicsSpaceSettings settings = new PhysicsSpaceSettings(resource.getSpaceSettings(spaceId)); + PhysicsCollisionLodSettings settings = PhysicsSpaces.collisionLodSettings(physicsStore, + selection.spaceRef()); + if (settings == null) { + ctx.sender().sendMessage(Message.raw("Physics space id=" + selection.spaceId().value() + + " no longer exists.")); + return CompletableFuture.completedFuture(null); + } if (!anyArgProvided(ctx)) { - sendSummary(ctx, spaceId, settings); + sendSummary(ctx, selection.spaceId(), settings); return CompletableFuture.completedFuture(null); } - Boolean enabled = settings.getCollisionLodSettings().isCollisionLodEnabled(); + Boolean enabled = settings.isCollisionLodEnabled(); if (enabledArg.provided(ctx)) { enabled = parseBoolean(enabledArg.get(ctx)); if (enabled == null) { @@ -91,7 +98,7 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, } } - Boolean farSleep = settings.getCollisionLodSettings().isCollisionLodFarSleepEnabled(); + Boolean farSleep = settings.isCollisionLodFarSleepEnabled(); if (farSleepArg.provided(ctx)) { farSleep = parseBoolean(farSleepArg.get(ctx)); if (farSleep == null) { @@ -102,16 +109,16 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, int nearRadius = nearRadiusArg.provided(ctx) ? nearRadiusArg.get(ctx) - : settings.getCollisionLodSettings().getCollisionLodNearRadius(); + : settings.getCollisionLodNearRadius(); int midRadius = midRadiusArg.provided(ctx) ? midRadiusArg.get(ctx) - : settings.getCollisionLodSettings().getCollisionLodMidRadius(); + : settings.getCollisionLodMidRadius(); int hysteresis = hysteresisArg.provided(ctx) ? hysteresisArg.get(ctx) - : settings.getCollisionLodSettings().getCollisionLodHysteresis(); + : settings.getCollisionLodHysteresis(); int interval = intervalArg.provided(ctx) ? intervalArg.get(ctx) - : settings.getCollisionLodSettings().getCollisionLodRefreshIntervalTicks(); + : settings.getCollisionLodRefreshIntervalTicks(); if (outOfRange(nearRadius, PhysicsCollisionLodSettings.MAX_COLLISION_LOD_RADIUS) || outOfRange(midRadius, PhysicsCollisionLodSettings.MAX_COLLISION_LOD_RADIUS) || nearRadius > midRadius @@ -129,13 +136,13 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, return CompletableFuture.completedFuture(null); } - settings.getCollisionLodSettings().setCollisionLodEnabled(enabled); - settings.getCollisionLodSettings().setCollisionLodRadii(nearRadius, midRadius); - settings.getCollisionLodSettings().setCollisionLodHysteresis(hysteresis); - settings.getCollisionLodSettings().setCollisionLodRefreshIntervalTicks(interval); - settings.getCollisionLodSettings().setCollisionLodFarSleepEnabled(farSleep); - resource.setSpaceSettings(spaceId, settings); - sendSummary(ctx, spaceId, settings); + settings.setCollisionLodEnabled(enabled); + settings.setCollisionLodRadii(nearRadius, midRadius); + settings.setCollisionLodHysteresis(hysteresis); + settings.setCollisionLodRefreshIntervalTicks(interval); + settings.setCollisionLodFarSleepEnabled(farSleep); + PhysicsSpaces.putCollisionLodSettings(physicsStore, selection.spaceRef(), settings); + sendSummary(ctx, selection.spaceId(), settings); return CompletableFuture.completedFuture(null); } @@ -154,15 +161,15 @@ private static boolean outOfRange(int value, int maxValue) { private static void sendSummary(@Nonnull CommandContext ctx, @Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceSettings settings) { + @Nonnull PhysicsCollisionLodSettings settings) { ctx.sender().sendMessage(Message.raw("Impulse collision LOD settings for space " + spaceId.value() - + ": enabled=" + settings.getCollisionLodSettings().isCollisionLodEnabled() - + " nearRadius=" + settings.getCollisionLodSettings().getCollisionLodNearRadius() - + " midRadius=" + settings.getCollisionLodSettings().getCollisionLodMidRadius() - + " hysteresis=" + settings.getCollisionLodSettings().getCollisionLodHysteresis() - + " interval=" + settings.getCollisionLodSettings().getCollisionLodRefreshIntervalTicks() - + " farSleep=" + settings.getCollisionLodSettings().isCollisionLodFarSleepEnabled() + + ": enabled=" + settings.isCollisionLodEnabled() + + " nearRadius=" + settings.getCollisionLodNearRadius() + + " midRadius=" + settings.getCollisionLodMidRadius() + + " hysteresis=" + settings.getCollisionLodHysteresis() + + " interval=" + settings.getCollisionLodRefreshIntervalTicks() + + " farSleep=" + settings.isCollisionLodFarSleepEnabled() + " tiers=near:terrain+body mid:terrain far:terrain+sleep")); } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkCommand.java new file mode 100644 index 00000000..29a25ced --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkCommand.java @@ -0,0 +1,12 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.commands; + +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection; + +public final class PhysicsChunkCommand extends AbstractCommandCollection { + + public PhysicsChunkCommand() { + super("physicschunk", "Impulse PhysicsChunk collision commands"); + addSubCommand(new PhysicsChunkSettingsCommand()); + addSubCommand(new PhysicsChunkPerfCommand()); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkCommandSet.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkCommandSet.java new file mode 100644 index 00000000..0827e89a --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkCommandSet.java @@ -0,0 +1,43 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.commands; + +import dev.hytalemodding.impulse.core.internal.commands.ImpulseCommandTreeRegistry; +import dev.hytalemodding.impulse.core.internal.commands.debug.DebugFlagCommand; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsDebugResource; + +/** + * Command set owned by the PhysicsChunk subplugin. + */ +public final class PhysicsChunkCommandSet { + + private static final String PHYSICS_CHUNK_ROOT_COMMAND_ID = "physicschunk.root"; + private static final String PHYSICS_CHUNK_DEBUG_COMMAND_ID = "physicschunk.debug"; + private static final String COLLISION_LOD_SETTINGS_COMMAND_ID = + "physicschunk.settings.collision-lod"; + + private PhysicsChunkCommandSet() { + } + + public static void register() { + ImpulseCommandTreeRegistry.registerRootAndSettingsSubCommands( + PHYSICS_CHUNK_ROOT_COMMAND_ID, + PhysicsChunkCommand::new, + COLLISION_LOD_SETTINGS_COMMAND_ID, + CollisionLodSettingsCommand::new); + ImpulseCommandTreeRegistry.registerDebugSubCommand( + PHYSICS_CHUNK_DEBUG_COMMAND_ID, + PhysicsChunkCommandSet::debugCommand); + } + + public static void unregister() { + ImpulseCommandTreeRegistry.unregisterRootAndSettingsSubCommands( + PHYSICS_CHUNK_ROOT_COMMAND_ID, + COLLISION_LOD_SETTINGS_COMMAND_ID); + ImpulseCommandTreeRegistry.unregisterDebugSubCommand(PHYSICS_CHUNK_DEBUG_COMMAND_ID); + } + + private static DebugFlagCommand debugCommand() { + return new DebugFlagCommand("physicschunk", "PhysicsChunk collision", + PhysicsDebugResource::isDebugPhysicsChunkCollisionEnabled, + PhysicsDebugResource::setDebugPhysicsChunkCollisionEnabled); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkPerfCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkPerfCommand.java new file mode 100644 index 00000000..7da80ecd --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkPerfCommand.java @@ -0,0 +1,13 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.commands; + +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection; + +public final class PhysicsChunkPerfCommand extends AbstractCommandCollection { + + public PhysicsChunkPerfCommand() { + super("perf", "Impulse runtime and PhysicsChunk profiling commands"); + addSubCommand(new PhysicsChunkPerfToggleCommand()); + addSubCommand(new PhysicsChunkPerfReportCommand()); + addSubCommand(new PhysicsChunkPerfResetCommand()); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionPerfReportCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkPerfReportCommand.java similarity index 68% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionPerfReportCommand.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkPerfReportCommand.java index 9da9b615..442c9c46 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionPerfReportCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkPerfReportCommand.java @@ -1,73 +1,80 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.commands; +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.commands; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.command.system.CommandContext; -import com.hypixel.hytale.server.core.command.system.basecommands.AbstractWorldCommand; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncWorldCommand; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.diagnostics.PhysicsEntityDiagnostics; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource.StepSnapshot; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource.SyncSnapshot; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource.VisualSnapshot; -import dev.hytalemodding.impulse.core.internal.simulation.query.PhysicsSpaceRuntimeStatsQuery; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsSpaceRuntimeStatsView; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyRegistrationView; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsCommandBatchEvent; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.SpaceSummary; -import dev.hytalemodding.impulse.core.plugin.simulation.query.SpaceSummaryQuery; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource.Snapshot; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsEntityDiagnostics; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsRuntimeProfiling; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsRuntimeProfiling.StepDrainSnapshotView; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsRuntimeProfiling.StepSnapshotView; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsRuntimeProfiling.SyncSnapshotView; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsRuntimeProfiling.VisualSnapshotView; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionProfiling; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsDiagnostics; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsAsync; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsWorlds; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.physics.SpaceSummary; import java.util.List; import java.util.Locale; +import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; -public class WorldCollisionPerfReportCommand extends AbstractWorldCommand { +public class PhysicsChunkPerfReportCommand extends AbstractAsyncWorldCommand { - public WorldCollisionPerfReportCommand() { - super("report", "Report Impulse world collision profiling metrics"); + public PhysicsChunkPerfReportCommand() { + super("report", "Report Impulse PhysicsChunk profiling metrics"); } + @Nonnull @Override - protected void execute(@Nonnull CommandContext ctx, + protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, + @Nonnull World world) { + Store store = world.getEntityStore().getStore(); + return PhysicsAsync.acceptOnWorldThread(world, + PhysicsDiagnostics.spaceSummariesAsync(world), + summaries -> sendReport(ctx, world, store, summaries)); + } + + private static void sendReport(@Nonnull CommandContext ctx, @Nonnull World world, - @Nonnull Store store) { - PhysicsRuntimeProfilingResource runtimeProfiling = store.getResource( - PhysicsRuntimeProfilingResource.getResourceType()); - StepSnapshot cumulativeStep = runtimeProfiling.getCumulativeStep(); - StepSnapshot latestStep = runtimeProfiling.getLatestStep(); - StepSnapshot latestCompletedStep = runtimeProfiling.getLatestCompletedStep(); - StepSnapshot worstStep = runtimeProfiling.getWorstStep(); - SyncSnapshot cumulativeSync = runtimeProfiling.getCumulativeSync(); - SyncSnapshot latestSync = runtimeProfiling.getLatestSync(); - SyncSnapshot worstSync = runtimeProfiling.getWorstSync(); - VisualSnapshot cumulativeVisual = runtimeProfiling.getCumulativeVisual(); - VisualSnapshot latestVisual = runtimeProfiling.getLatestVisual(); - VisualSnapshot worstVisual = runtimeProfiling.getWorstVisual(); - WorldCollisionProfilingResource profiling = store.getResource( - WorldCollisionProfilingResource.getResourceType()); - Snapshot cumulative = profiling.getCumulativeSnapshot(); - Snapshot latest = profiling.getLatestTickSnapshot(); - Snapshot worst = profiling.getWorstTickSnapshot(); + @Nonnull Store store, + @Nonnull List summaries) { + PhysicsRuntimeProfiling.Snapshots runtimeProfiling = PhysicsRuntimeProfiling.snapshots(store); + StepSnapshotView cumulativeStep = runtimeProfiling.cumulativeStep(); + StepSnapshotView latestStep = runtimeProfiling.latestStep(); + StepSnapshotView latestCompletedStep = runtimeProfiling.latestCompletedStep(); + StepSnapshotView worstStep = runtimeProfiling.worstStep(); + SyncSnapshotView cumulativeSync = runtimeProfiling.cumulativeSync(); + SyncSnapshotView latestSync = runtimeProfiling.latestSync(); + SyncSnapshotView worstSync = runtimeProfiling.worstSync(); + VisualSnapshotView cumulativeVisual = runtimeProfiling.cumulativeVisual(); + VisualSnapshotView latestVisual = runtimeProfiling.latestVisual(); + VisualSnapshotView worstVisual = runtimeProfiling.worstVisual(); + PhysicsChunkCollisionProfiling.Snapshots profiling = + PhysicsChunkCollisionProfiling.snapshots(store); + var cumulative = profiling.cumulative(); + var latest = profiling.latest(); + var worst = profiling.worst(); PhysicsEntityDiagnostics.Snapshot entityDiagnostics = PhysicsEntityDiagnostics.collect(store); - PhysicsWorldResource physicsWorld = store.getResource(PhysicsWorldResource.getResourceType()); - RuntimeFootprint runtimeFootprint = RuntimeFootprint.collect(physicsWorld); + Store physicsStore = PhysicsThreading.store(world); + RuntimeFootprint runtimeFootprint = RuntimeFootprint.collect(summaries); ctx.sender().sendMessage(Message.raw("Impulse runtime profiling: " - + ((runtimeProfiling.isEnabled() || profiling.isEnabled()) ? "enabled" : "disabled"))); + + ((runtimeProfiling.enabled() || profiling.enabled()) ? "enabled" : "disabled"))); ctx.sender().sendMessage(Message.raw("Impulse runtime physics: " + runtimeFootprint.summary())); if (runtimeFootprint.hasRuntimeStats()) { ctx.sender().sendMessage(Message.raw("Physics backend runtime stats: " - + runtimeFootprint.runtimeStatsSummary())); + + formatRuntimeStatsSummary(runtimeFootprint))); } ctx.sender().sendMessage(Message.raw("Physics event frame: " - + formatEventFrameSummary(physicsWorld.getLatestEventFrame()))); + + formatEventFrameSummary(PhysicsWorlds.latestEventFrame(physicsStore)))); ctx.sender().sendMessage(Message.raw("Hytale entity diagnostics: " + entityDiagnostics.hytaleSummary())); ctx.sender().sendMessage(Message.raw("Impulse entity diagnostics: " @@ -85,16 +92,16 @@ protected void execute(@Nonnull CommandContext ctx, + " indexCells=" + formatAverage(cumulativeStep.getSpatialIndexCells(), cumulativeStep.getTickSamples()))); ctx.sender().sendMessage(Message.raw("Physics snapshot avg ms/completedStep=" + formatAverageMillis(cumulativeStep.getSnapshotNanos(), cumulativeStep.getTickSamples()))); - ctx.sender().sendMessage(Message.raw("Physics owner step avg queued/run/latency ms=" - + formatAverageMillis(cumulativeStep.getOwnerQueuedNanos(), cumulativeStep.getTickSamples()) - + "/" + formatAverageMillis(cumulativeStep.getOwnerRunNanos(), cumulativeStep.getTickSamples()) + ctx.sender().sendMessage(Message.raw("Physics store tick avg queued/run/latency ms=" + + formatAverageMillis(cumulativeStep.getStoreTickQueuedNanos(), cumulativeStep.getTickSamples()) + + "/" + formatAverageMillis(cumulativeStep.getStoreTickRunNanos(), cumulativeStep.getTickSamples()) + "/" + formatAverageMillis( - cumulativeStep.getOwnerQueuedNanos() + cumulativeStep.getOwnerRunNanos(), + cumulativeStep.getStoreTickQueuedNanos() + cumulativeStep.getStoreTickRunNanos(), cumulativeStep.getTickSamples()) - + " ownerTPS latest/avg=" + formatHertz(latestStep.getOwnerStepIntervalNanos()) - + "/" + formatAverageHertz(cumulativeStep.getOwnerStepIntervalNanos(), - cumulativeStep.getOwnerStepRateSamples()) - + " maxGapMs=" + formatMillis(cumulativeStep.getMaxOwnerStepIntervalNanos()) + + " storeTPS latest/avg=" + formatHertz(latestStep.getStoreTickStepIntervalNanos()) + + "/" + formatAverageHertz(cumulativeStep.getStoreTickStepIntervalNanos(), + cumulativeStep.getStoreTickStepRateSamples()) + + " maxGapMs=" + formatMillis(cumulativeStep.getMaxStoreTickStepIntervalNanos()) + " pendingSkips=" + cumulativeStep.getSkippedPendingSteps() + " pendingAge avg/max ms=" + formatAverageMillis(cumulativeStep.getPendingStepAgeNanos(), @@ -221,21 +228,21 @@ protected void execute(@Nonnull CommandContext ctx, } } else { ctx.sender().sendMessage(Message.raw("No profiled physics step/sync/visual ticks recorded yet." - + (runtimeProfiling.isEnabled() + + (runtimeProfiling.enabled() ? "" - : " Run /impulse worldcollision perf toggle, wait a few seconds, then run /impulse worldcollision perf report."))); + : " Run /impulse physicschunk perf toggle, wait a few seconds, then run /impulse physicschunk perf report."))); } if (cumulative.getTickSamples() <= 0) { - ctx.sender().sendMessage(Message.raw("No profiled world collision ticks recorded yet." - + (profiling.isEnabled() + ctx.sender().sendMessage(Message.raw("No profiled PhysicsChunk ticks recorded yet." + + (profiling.enabled() ? "" - : " Run /impulse worldcollision perf toggle, wait a few seconds, then run /impulse worldcollision perf report."))); + : " Run /impulse physicschunk perf toggle, wait a few seconds, then run /impulse physicschunk perf report."))); return; } - ctx.sender().sendMessage(Message.raw("World collision profiling: " - + (profiling.isEnabled() ? "enabled" : "disabled"))); + ctx.sender().sendMessage(Message.raw("PhysicsChunk profiling: " + + (profiling.enabled() ? "enabled" : "disabled"))); ctx.sender().sendMessage(Message.raw("Since reset: ticks=" + cumulative.getTickSamples() + " playerTargets=" + cumulative.getPlayerStreamingTargets() @@ -243,8 +250,8 @@ protected void execute(@Nonnull CommandContext ctx, + "/" + cumulative.getBodySpatialIndexCandidates() + "/" + cumulative.getBodyStreamingTargets() + " spaces=" + cumulative.getStreamingSpaces() - + " terrainApply queued/skipped=" + cumulative.getTerrainApplyQueued() - + "/" + cumulative.getTerrainApplySkippedPending() + + " collisionApply queued/skipped=" + cumulative.getCollisionApplyQueued() + + "/" + cumulative.getCollisionApplySkippedPending() + " sectionTargets player/body=" + cumulative.getPlayerSectionTargets() + "/" + cumulative.getBodySectionTargets() + " ensureCalls=" + cumulative.getEnsureCalls() @@ -278,9 +285,11 @@ protected void execute(@Nonnull CommandContext ctx, + cumulative.getMissingInsideRetainedEnvelope() + "/" + cumulative.getMissingOutsideRetainedEnvelope() + "/" + cumulative.getMissingUnconfiguredRetainedEnvelope())); - if (!cumulative.getMissingSectionSamples().isEmpty()) { + List missingSectionSamples = + PhysicsChunkCollisionProfiling.missingSectionSamples(cumulative); + if (!missingSectionSamples.isEmpty()) { ctx.sender().sendMessage(Message.raw("Missing section samples: " - + formatMissingSectionSamples(cumulative))); + + formatMissingSectionSamples(missingSectionSamples))); } ctx.sender().sendMessage(Message.raw("Since reset bodies+blocks: added=" + cumulative.getColliderBodiesAdded() @@ -307,8 +316,8 @@ protected void execute(@Nonnull CommandContext ctx, + "/" + latest.getBodySpatialIndexCandidates() + "/" + latest.getBodyStreamingTargets() + " spaces=" + latest.getStreamingSpaces() - + " terrainApply queued/skipped=" + latest.getTerrainApplyQueued() - + "/" + latest.getTerrainApplySkippedPending() + + " collisionApply queued/skipped=" + latest.getCollisionApplyQueued() + + "/" + latest.getCollisionApplySkippedPending() + " sectionTargets player/body=" + latest.getPlayerSectionTargets() + "/" + latest.getBodySectionTargets() + " ensure=" + latest.getEnsureCalls() @@ -330,8 +339,8 @@ protected void execute(@Nonnull CommandContext ctx, + "/" + worst.getBodySpatialIndexCandidates() + "/" + worst.getBodyStreamingTargets() + " spaces=" + worst.getStreamingSpaces() - + " terrainApply queued/skipped=" + worst.getTerrainApplyQueued() - + "/" + worst.getTerrainApplySkippedPending() + + " collisionApply queued/skipped=" + worst.getCollisionApplyQueued() + + "/" + worst.getCollisionApplySkippedPending() + " sectionTargets player/body=" + worst.getPlayerSectionTargets() + "/" + worst.getBodySectionTargets() + " ensure=" + worst.getEnsureCalls() @@ -350,11 +359,11 @@ protected void execute(@Nonnull CommandContext ctx, } @Nonnull - private static String formatMissingSectionSamples(@Nonnull Snapshot snapshot) { + private static String formatMissingSectionSamples( + @Nonnull List samples) { StringBuilder builder = new StringBuilder(); int emitted = 0; - for (WorldCollisionProfilingResource.MissingSectionSample sample - : snapshot.getMissingSectionSamples()) { + for (PhysicsChunkCollisionProfiling.MissingSectionSampleView sample : samples) { if (emitted > 0) { builder.append(" | "); } @@ -365,22 +374,22 @@ private static String formatMissingSectionSamples(@Nonnull Snapshot snapshot) { .append("/") .append(sample.chunkZ()) .append(" reason=") - .append(sample.reason().name().toLowerCase(Locale.ROOT)) + .append(sample.reason()) .append(" retained=") - .append(sample.retainedEnvelopeStatus().name().toLowerCase(Locale.ROOT)) + .append(sample.retainedEnvelopeStatus()) .append(" target=") - .append(sample.target().targetType().name().toLowerCase(Locale.ROOT)); - if (sample.target().bodyKey() != null) { - builder.append(" body=").append(sample.target().bodyKey()); + .append(sample.targetType()); + if (sample.bodyUuid() != null) { + builder.append(" body=").append(sample.bodyUuid()); } - if (sample.target().snapshotPosition() != null) { + if (sample.snapshotPosition() != null) { builder.append(" snapshot=(") - .append(sample.target().snapshotPosition().compact()) + .append(sample.snapshotPosition()) .append(")"); } - if (sample.target().livePosition() != null) { + if (sample.livePosition() != null) { builder.append(" live=(") - .append(sample.target().livePosition().compact()) + .append(sample.livePosition()) .append(")"); } emitted++; @@ -388,9 +397,9 @@ private static String formatMissingSectionSamples(@Nonnull Snapshot snapshot) { break; } } - if (snapshot.getMissingSectionSamples().size() > emitted) { + if (samples.size() > emitted) { builder.append(" | +") - .append(snapshot.getMissingSectionSamples().size() - emitted) + .append(samples.size() - emitted) .append(" more"); } return builder.toString(); @@ -409,12 +418,8 @@ static String formatEventFrameSummary(@Nonnull PhysicsEventFrame frame) { .append(frame.latestCapturedSnapshotStepSequence()) .append(" latestCapturedSnapshotTick=") .append(frame.latestCapturedSnapshotServerTick()) - .append(" latestCapturedSnapshotLastIncludedCommandBatch=") - .append(frame.latestCapturedSnapshotLastIncludedCommandBatchSequence()) .append(" events=") .append(frame.eventCount()) - .append(" commandBatches=") - .append(frame.commandBatchCount()) .append(" steps=") .append(frame.stepCount()) .append(" publications=") @@ -423,36 +428,6 @@ static String formatEventFrameSummary(@Nonnull PhysicsEventFrame frame) { .append(frame.physicsEventCount()) .append(" droppedBackendEvents=") .append(frame.droppedBackendEventCount()); - PhysicsCommandBatchEvent latestCommand = frame.latestCommandBatch(); - if (latestCommand != null) { - boolean capturedSnapshotIncluded = frame.latestCapturedSnapshotIncludes(latestCommand); - builder.append(" latestCommand=") - .append(latestCommand.commandBatchSequence()) - .append(" submittedTick=") - .append(latestCommand.submittedServerTick()) - .append(" bodyRefs=") - .append(latestCommand.bodyKeyReferenceCount()) - .append(" jointRefs=") - .append(latestCommand.jointKeyReferenceCount()) - .append(" capturedSnapshotIncluded=") - .append(capturedSnapshotIncluded); - if (latestCommand.firstBodyKey() != null) { - builder.append(" firstBody=") - .append(latestCommand.firstBodyKey()); - } - if (latestCommand.firstJointKey() != null) { - builder.append(" firstJoint=") - .append(latestCommand.firstJointKey()); - } - if (capturedSnapshotIncluded) { - builder.append(" capturedSnapshotTickLatency=") - .append(frame.capturedSnapshotServerTickLatency(latestCommand)); - } - if (!latestCommand.allApplied()) { - builder.append(" firstRejected=") - .append(latestCommand.firstRejectedCommandSequence()); - } - } var latestPublication = frame.latestSnapshotPublication(); if (latestPublication != null) { builder.append(" publishedTick=") @@ -464,8 +439,8 @@ static String formatEventFrameSummary(@Nonnull PhysicsEventFrame frame) { } @Nonnull - static String formatPreStepDrainSummary(@Nonnull StepSnapshot cumulativeStep, - @Nonnull StepSnapshot latestStep) { + static String formatPreStepDrainSummary(@Nonnull StepDrainSnapshotView cumulativeStep, + @Nonnull StepDrainSnapshotView latestStep) { return "Physics pre-step drain avg completedStep drained/runMs/lateBacklog=" + formatAverage(cumulativeStep.getPreStepDrainedMutations(), cumulativeStep.getTickSamples()) @@ -481,10 +456,50 @@ static String formatPreStepDrainSummary(@Nonnull StepSnapshot cumulativeStep, + "/" + cumulativeStep.getMaxLateMutationBacklogAtStep(); } - static boolean hasCompletedStepSamples(@Nonnull StepSnapshot cumulativeStep) { + static boolean hasCompletedStepSamples(@Nonnull StepDrainSnapshotView cumulativeStep) { return cumulativeStep.getTickSamples() > 0; } + @Nonnull + static String formatRuntimeStatsSummary(@Nonnull RuntimeStatsView stats) { + return "spaces=" + stats.runtimeStatsSpaces() + + " bodies=" + stats.runtimeBodies() + + " colliders=" + stats.runtimeColliders() + + " activeBodies=" + stats.runtimeActiveBodies() + + " activeIslands=" + stats.runtimeActiveIslands() + + " contactPairs=" + stats.runtimeContactPairs() + + " contactManifolds=" + stats.runtimeContactManifolds() + + " contactPoints=" + stats.runtimeContactPoints() + + " dynamicDynamicPairs=" + stats.runtimeDynamicDynamicContactPairs() + + " terrainPairs=" + stats.runtimeTerrainContactPairs() + + " joints=" + stats.runtimeJoints(); + } + + interface RuntimeStatsView { + + int runtimeStatsSpaces(); + + int runtimeBodies(); + + int runtimeColliders(); + + int runtimeActiveBodies(); + + int runtimeContactPairs(); + + int runtimeContactManifolds(); + + int runtimeContactPoints(); + + int runtimeDynamicDynamicContactPairs(); + + int runtimeTerrainContactPairs(); + + int runtimeActiveIslands(); + + int runtimeJoints(); + } + @Nonnull private static String formatMillis(long nanos) { return String.format(Locale.ROOT, "%.3f", nanos / 1_000_000.0); @@ -551,25 +566,24 @@ private static String formatAverageHertz(long totalIntervalNanos, int samples) { } private record RuntimeFootprint(int spaces, - int backendBodies, - int backendJoints, - int detachedBodies, - int detachedVisualProxies, - int runtimeStatsSpaces, - int runtimeBodies, - int runtimeColliders, - int runtimeActiveBodies, - int runtimeContactPairs, - int runtimeContactManifolds, - int runtimeContactPoints, - int runtimeDynamicDynamicContactPairs, - int runtimeTerrainContactPairs, - int runtimeActiveIslands, - int runtimeJoints) { + int backendBodies, + int backendJoints, + int detachedBodies, + int detachedVisualProxies, + int runtimeStatsSpaces, + int runtimeBodies, + int runtimeColliders, + int runtimeActiveBodies, + int runtimeContactPairs, + int runtimeContactManifolds, + int runtimeContactPoints, + int runtimeDynamicDynamicContactPairs, + int runtimeTerrainContactPairs, + int runtimeActiveIslands, + int runtimeJoints) implements RuntimeStatsView { @Nonnull - private static RuntimeFootprint collect(@Nonnull PhysicsWorldResource resource) { - PhysicsWorldRuntimeResource runtime = PhysicsWorldRuntimeResource.require(resource); + private static RuntimeFootprint collect(@Nonnull List summaries) { int spaces = 0; int backendBodies = 0; int backendJoints = 0; @@ -584,51 +598,31 @@ private static RuntimeFootprint collect(@Nonnull PhysicsWorldResource resource) int runtimeTerrainContactPairs = 0; int runtimeActiveIslands = 0; int runtimeJoints = 0; - List summaries = resource.query(new SpaceSummaryQuery(null)) - .completion() - .toCompletableFuture() - .join(); for (SpaceSummary summary : summaries) { - PhysicsSpaceRuntimeStatsView stats = runtime.queryInternal( - new PhysicsSpaceRuntimeStatsQuery(summary.spaceId())) - .toCompletableFuture() - .join(); spaces++; - backendBodies += stats.bodies(); - backendJoints += stats.joints(); - if (stats.runtimeStatsAvailable()) { + backendBodies += summary.bodyCount(); + backendJoints += summary.jointCount(); + if (summary.runtimeStatsAvailable()) { runtimeStatsSpaces++; - runtimeBodies += stats.runtimeBodyCount(); - runtimeColliders += stats.runtimeColliderCount(); - runtimeActiveBodies += stats.runtimeActiveBodyCount(); - runtimeContactPairs += stats.runtimeContactPairCount(); - runtimeContactManifolds += stats.runtimeContactManifoldCount(); - runtimeContactPoints += stats.runtimeContactPointCount(); - runtimeDynamicDynamicContactPairs += stats.runtimeDynamicDynamicContactPairCount(); - runtimeTerrainContactPairs += stats.runtimeTerrainContactPairCount(); - runtimeActiveIslands += stats.runtimeActiveIslandCount(); - runtimeJoints += stats.runtimeJointCount(); - } - } - - int detachedBodies = 0; - int detachedVisualProxies = 0; - for (PhysicsBodyRegistrationView registration : resource.getBodyRegistrationViews()) { - if (registration.kind() != PhysicsBodyKind.BODY - || resource.hasBodyAttachments(registration.bodyKey())) { - continue; - } - detachedBodies++; - if (runtime.getGeneratedVisualProxy(registration.bodyKey()) != null) { - detachedVisualProxies++; + runtimeBodies += summary.runtimeBodyCount(); + runtimeColliders += summary.runtimeColliderCount(); + runtimeActiveBodies += summary.runtimeActiveBodyCount(); + runtimeContactPairs += summary.runtimeContactPairCount(); + runtimeContactManifolds += summary.runtimeContactManifoldCount(); + runtimeContactPoints += summary.runtimeContactPointCount(); + runtimeDynamicDynamicContactPairs += + summary.runtimeDynamicDynamicContactPairCount(); + runtimeTerrainContactPairs += summary.runtimeTerrainContactPairCount(); + runtimeActiveIslands += summary.runtimeActiveIslandCount(); + runtimeJoints += summary.runtimeJointCount(); } } return new RuntimeFootprint(spaces, backendBodies, backendJoints, - detachedBodies, - detachedVisualProxies, + 0, + 0, runtimeStatsSpaces, runtimeBodies, runtimeColliders, @@ -647,27 +641,12 @@ private String summary() { return "spaces=" + spaces + " backendBodies=" + backendBodies + " backendJoints=" + backendJoints - + " detachedBodies=" + detachedBodies - + " detachedVisualProxies=" + detachedVisualProxies; + + " detachedBodies=unavailable" + + " detachedVisualProxies=unavailable"; } private boolean hasRuntimeStats() { return runtimeStatsSpaces > 0; } - - @Nonnull - private String runtimeStatsSummary() { - return "spaces=" + runtimeStatsSpaces - + " bodies=" + runtimeBodies - + " colliders=" + runtimeColliders - + " activeBodies=" + runtimeActiveBodies - + " activeIslands=" + runtimeActiveIslands - + " contactPairs=" + runtimeContactPairs - + " contactManifolds=" + runtimeContactManifolds - + " contactPoints=" + runtimeContactPoints - + " dynamicDynamicPairs=" + runtimeDynamicDynamicContactPairs - + " terrainPairs=" + runtimeTerrainContactPairs - + " joints=" + runtimeJoints; - } } } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkPerfResetCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkPerfResetCommand.java new file mode 100644 index 00000000..2d69063f --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkPerfResetCommand.java @@ -0,0 +1,25 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.commands; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractWorldCommand; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionProfiling; +import javax.annotation.Nonnull; + +public class PhysicsChunkPerfResetCommand extends AbstractWorldCommand { + + public PhysicsChunkPerfResetCommand() { + super("reset", "Reset Impulse runtime profiling counters"); + } + + @Override + protected void execute(@Nonnull CommandContext ctx, + @Nonnull World world, + @Nonnull Store store) { + PhysicsChunkCollisionProfiling.resetRuntimeProfiling(world, store); + ctx.sender().sendMessage(Message.raw("Impulse runtime profiling counters reset")); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkPerfToggleCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkPerfToggleCommand.java new file mode 100644 index 00000000..340fda5c --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkPerfToggleCommand.java @@ -0,0 +1,27 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.commands; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractWorldCommand; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionProfiling; +import javax.annotation.Nonnull; + +public class PhysicsChunkPerfToggleCommand extends AbstractWorldCommand { + + public PhysicsChunkPerfToggleCommand() { + super("toggle", "Toggle Impulse runtime profiling"); + } + + @Override + protected void execute(@Nonnull CommandContext ctx, + @Nonnull World world, + @Nonnull Store store) { + boolean enabled = !PhysicsChunkCollisionProfiling.isRuntimeProfilingEnabled(store); + PhysicsChunkCollisionProfiling.setRuntimeProfilingEnabled(world, store, enabled); + ctx.sender().sendMessage(Message.raw("Impulse runtime profiling " + + (enabled ? "enabled" : "disabled"))); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionSettingsCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkSettingsCommand.java similarity index 52% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionSettingsCommand.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkSettingsCommand.java index 11d6a4ad..63c184a8 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionSettingsCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkSettingsCommand.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.commands; +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.commands; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; @@ -10,57 +10,57 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.commands.SpaceSelection; -import dev.hytalemodding.impulse.core.plugin.settings.EntityChunkBoundaryMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldCollisionSettings; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionMode; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsSpaces; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.EntityChunkBoundaryMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsChunkCollisionSettings; import java.util.Locale; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; import javax.annotation.Nullable; -public class WorldCollisionSettingsCommand extends AbstractAsyncPlayerCommand { +public class PhysicsChunkSettingsCommand extends AbstractAsyncPlayerCommand { private final OptionalArg modeArg = this.withOptionalArg( "mode", - "World collision mode: none, manual, or streaming", + "PhysicsChunk mode: none, manual, or streaming", ArgTypes.STRING); private final OptionalArg playerRadiusArg = this.withOptionalArg( "playerRadius", "Block radius streamed around players (1-" - + PhysicsWorldCollisionSettings.MAX_WORLD_COLLISION_RADIUS + + PhysicsChunkCollisionSettings.MAX_RADIUS + ")", ArgTypes.INTEGER); private final OptionalArg bodyRadiusArg = this.withOptionalArg( "bodyRadius", "Block radius streamed around awake dynamic bodies (1-" - + PhysicsWorldCollisionSettings.MAX_WORLD_COLLISION_BODY_RADIUS + + PhysicsChunkCollisionSettings.MAX_BODY_RADIUS + ")", ArgTypes.INTEGER); private final OptionalArg ttlArg = this.withOptionalArg( "ttl", "Ticks before unused streamed sections are pruned (1-" - + PhysicsWorldCollisionSettings.MAX_WORLD_COLLISION_TTL_TICKS + + PhysicsChunkCollisionSettings.MAX_TTL_TICKS + ")", ArgTypes.INTEGER); private final OptionalArg chunkBoundaryArg = this.withOptionalArg( "chunkBoundary", "Entity body chunk-boundary mode: pause or load", ArgTypes.STRING); - private final OptionalArg terrainArg = this.withOptionalArg( - "terrain", - "World collision terrain collider: boxes or native_voxels", + private final OptionalArg collisionShapeArg = this.withOptionalArg( + "collisionShape", + "PhysicsChunk collision shape: boxes or native_voxels", ArgTypes.STRING); private final OptionalArg spaceArg = this.withOptionalArg( "space", "Physics space id to target", ArgTypes.INTEGER); - public WorldCollisionSettingsCommand() { - super("settings", "Get or set world collision streaming settings for a physics space"); + public PhysicsChunkSettingsCommand() { + super("settings", "Get or set PhysicsChunk collision settings for a physics space"); } @Nonnull @@ -70,19 +70,26 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); - SpaceId spaceId = SpaceSelection.resolve(ctx, world, resource, spaceArg); - if (spaceId == null) { + Store physicsStore = PhysicsThreading.store(world); + PhysicsChunkSpaceSelection.Selection selection = + PhysicsChunkSpaceSelection.resolve(ctx, world, spaceArg, physicsStore); + if (selection == null) { return CompletableFuture.completedFuture(null); } - PhysicsSpaceSettings settings = new PhysicsSpaceSettings(resource.getSpaceSettings(spaceId)); + PhysicsChunkCollisionSettings settings = PhysicsSpaces.chunkCollisionSettings(physicsStore, + selection.spaceRef()); + if (settings == null) { + ctx.sender().sendMessage(Message.raw("Physics space id=" + selection.spaceId().value() + + " no longer exists.")); + return CompletableFuture.completedFuture(null); + } if (!anyArgProvided(ctx)) { - sendSummary(ctx, spaceId, settings); + sendSummary(ctx, selection.spaceId(), settings); return CompletableFuture.completedFuture(null); } - WorldCollisionMode mode = settings.getWorldCollisionSettings().getWorldCollisionMode(); + PhysicsChunkCollisionMode mode = settings.getMode(); if (modeArg.provided(ctx)) { mode = parseMode(modeArg.get(ctx)); if (mode == null) { @@ -91,7 +98,7 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, } } - EntityChunkBoundaryMode chunkBoundaryMode = settings.getWorldCollisionSettings().getEntityChunkBoundaryMode(); + EntityChunkBoundaryMode chunkBoundaryMode = settings.getEntityChunkBoundaryMode(); if (chunkBoundaryArg.provided(ctx)) { chunkBoundaryMode = parseChunkBoundaryMode(chunkBoundaryArg.get(ctx)); if (chunkBoundaryMode == null) { @@ -100,44 +107,46 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, } } - boolean nativeVoxelTerrainEnabled = settings.getWorldCollisionSettings().isNativeVoxelTerrainEnabled(); - if (terrainArg.provided(ctx)) { - Boolean parsedTerrain = parseTerrain(terrainArg.get(ctx)); - if (parsedTerrain == null) { - ctx.sender().sendMessage(Message.raw("terrain must be boxes or native_voxels.")); + boolean nativeVoxelCollisionEnabled = settings.isNativeVoxelCollisionEnabled(); + if (collisionShapeArg.provided(ctx)) { + Boolean parsedNativeVoxelCollision = + parseCollisionShape(collisionShapeArg.get(ctx)); + if (parsedNativeVoxelCollision == null) { + ctx.sender().sendMessage(Message.raw( + "collisionShape must be boxes or native_voxels.")); return CompletableFuture.completedFuture(null); } - nativeVoxelTerrainEnabled = parsedTerrain; + nativeVoxelCollisionEnabled = parsedNativeVoxelCollision; } int playerRadius = playerRadiusArg.provided(ctx) ? playerRadiusArg.get(ctx) - : settings.getWorldCollisionSettings().getWorldCollisionRadius(); + : settings.getRadius(); int bodyRadius = bodyRadiusArg.provided(ctx) ? bodyRadiusArg.get(ctx) - : settings.getWorldCollisionSettings().getWorldCollisionBodyRadius(); - int ttl = ttlArg.provided(ctx) ? ttlArg.get(ctx) : settings.getWorldCollisionSettings().getWorldCollisionTtlTicks(); - if (outOfRange(playerRadius, PhysicsWorldCollisionSettings.MAX_WORLD_COLLISION_RADIUS) - || outOfRange(bodyRadius, PhysicsWorldCollisionSettings.MAX_WORLD_COLLISION_BODY_RADIUS) - || outOfRange(ttl, PhysicsWorldCollisionSettings.MAX_WORLD_COLLISION_TTL_TICKS)) { + : settings.getBodyRadius(); + int ttl = ttlArg.provided(ctx) ? ttlArg.get(ctx) : settings.getTtlTicks(); + if (outOfRange(playerRadius, PhysicsChunkCollisionSettings.MAX_RADIUS) + || outOfRange(bodyRadius, PhysicsChunkCollisionSettings.MAX_BODY_RADIUS) + || outOfRange(ttl, PhysicsChunkCollisionSettings.MAX_TTL_TICKS)) { ctx.sender().sendMessage(Message.raw( - "playerRadius must be 1-" + PhysicsWorldCollisionSettings.MAX_WORLD_COLLISION_RADIUS + "playerRadius must be 1-" + PhysicsChunkCollisionSettings.MAX_RADIUS + ", bodyRadius must be 1-" - + PhysicsWorldCollisionSettings.MAX_WORLD_COLLISION_BODY_RADIUS + + PhysicsChunkCollisionSettings.MAX_BODY_RADIUS + ", and ttl must be 1-" - + PhysicsWorldCollisionSettings.MAX_WORLD_COLLISION_TTL_TICKS + + PhysicsChunkCollisionSettings.MAX_TTL_TICKS + ".")); return CompletableFuture.completedFuture(null); } - settings.getWorldCollisionSettings().setWorldCollisionMode(mode); - settings.getWorldCollisionSettings().setEntityChunkBoundaryMode(chunkBoundaryMode); - settings.getWorldCollisionSettings().setNativeVoxelTerrainEnabled(nativeVoxelTerrainEnabled); - settings.getWorldCollisionSettings().setWorldCollisionRadius(playerRadius); - settings.getWorldCollisionSettings().setWorldCollisionBodyRadius(bodyRadius); - settings.getWorldCollisionSettings().setWorldCollisionTtlTicks(ttl); - resource.setSpaceSettings(spaceId, settings); - sendSummary(ctx, spaceId, settings); + settings.setMode(mode); + settings.setEntityChunkBoundaryMode(chunkBoundaryMode); + settings.setNativeVoxelCollisionEnabled(nativeVoxelCollisionEnabled); + settings.setRadius(playerRadius); + settings.setBodyRadius(bodyRadius); + settings.setTtlTicks(ttl); + PhysicsSpaces.putChunkCollisionSettings(physicsStore, selection.spaceRef(), settings); + sendSummary(ctx, selection.spaceId(), settings); return CompletableFuture.completedFuture(null); } @@ -147,7 +156,7 @@ private boolean anyArgProvided(@Nonnull CommandContext ctx) { || bodyRadiusArg.provided(ctx) || ttlArg.provided(ctx) || chunkBoundaryArg.provided(ctx) - || terrainArg.provided(ctx); + || collisionShapeArg.provided(ctx); } private static boolean outOfRange(int value, int maxValue) { @@ -156,27 +165,27 @@ private static boolean outOfRange(int value, int maxValue) { private static void sendSummary(@Nonnull CommandContext ctx, @Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceSettings settings) { - ctx.sender().sendMessage(Message.raw("Impulse world collision settings for space " + @Nonnull PhysicsChunkCollisionSettings settings) { + ctx.sender().sendMessage(Message.raw("Impulse PhysicsChunk settings for space " + spaceId.value() - + ": mode=" + settings.getWorldCollisionSettings().getWorldCollisionMode().name().toLowerCase(Locale.ROOT) - + " playerRadius=" + settings.getWorldCollisionSettings().getWorldCollisionRadius() - + " bodyRadius=" + settings.getWorldCollisionSettings().getWorldCollisionBodyRadius() - + " ttl=" + settings.getWorldCollisionSettings().getWorldCollisionTtlTicks() + + ": mode=" + settings.getMode().name().toLowerCase(Locale.ROOT) + + " playerRadius=" + settings.getRadius() + + " bodyRadius=" + settings.getBodyRadius() + + " ttl=" + settings.getTtlTicks() + " chunkBoundary=" - + settings.getWorldCollisionSettings().getEntityChunkBoundaryMode().name().toLowerCase(Locale.ROOT) - + " terrain=" - + (settings.getWorldCollisionSettings().isNativeVoxelTerrainEnabled() + + settings.getEntityChunkBoundaryMode().name().toLowerCase(Locale.ROOT) + + " collisionShape=" + + (settings.isNativeVoxelCollisionEnabled() ? "native_voxels" : "boxes"))); } @Nullable - private static WorldCollisionMode parseMode(@Nonnull String value) { + private static PhysicsChunkCollisionMode parseMode(@Nonnull String value) { return switch (value.toLowerCase(Locale.ROOT)) { - case "none", "off", "disabled" -> WorldCollisionMode.NONE; - case "manual" -> WorldCollisionMode.MANUAL; - case "streaming", "stream", "on", "enabled" -> WorldCollisionMode.STREAMING; + case "none", "off", "disabled" -> PhysicsChunkCollisionMode.NONE; + case "manual" -> PhysicsChunkCollisionMode.MANUAL; + case "streaming", "stream", "on", "enabled" -> PhysicsChunkCollisionMode.STREAMING; default -> null; }; } @@ -191,7 +200,7 @@ private static EntityChunkBoundaryMode parseChunkBoundaryMode(@Nonnull String va } @Nullable - private static Boolean parseTerrain(@Nonnull String value) { + private static Boolean parseCollisionShape(@Nonnull String value) { return switch (value.toLowerCase(Locale.ROOT)) { case "boxes", "box", "merged", "merged_boxes" -> Boolean.FALSE; case "native", "native_voxels", "voxels", "voxel" -> Boolean.TRUE; diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/SpaceSelection.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkSpaceSelection.java similarity index 50% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/SpaceSelection.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkSpaceSelection.java index 442b444f..a1ad0923 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/SpaceSelection.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkSpaceSelection.java @@ -1,62 +1,62 @@ -package dev.hytalemodding.impulse.core.internal.commands; +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.commands; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.command.system.CommandContext; import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsSpaces; import java.util.Comparator; import javax.annotation.Nonnull; import javax.annotation.Nullable; -public final class SpaceSelection { +final class PhysicsChunkSpaceSelection { - private SpaceSelection() { + private PhysicsChunkSpaceSelection() { } @Nullable - public static SpaceId resolve(@Nonnull CommandContext context, + static Selection resolve(@Nonnull CommandContext context, @Nonnull World world, - @Nonnull PhysicsWorldResource resource, - @Nonnull OptionalArg spaceArg) { + @Nonnull OptionalArg spaceArg, + @Nonnull Store physicsStore) { if (spaceArg.provided(context)) { int rawSpaceId = spaceArg.get(context); if (rawSpaceId <= 0) { context.sendMessage(Message.raw("Space id must be a positive integer.")); return null; } - SpaceId spaceId = specifiedSpaceId(resource, rawSpaceId); - if (spaceId == null) { + SpaceId spaceId = new SpaceId(rawSpaceId); + Ref spaceRef = PhysicsSpaces.resolveRef(physicsStore, spaceId); + if (spaceRef == null) { context.sendMessage(Message.raw("No physics space id=" + rawSpaceId + " exists in world " + world.getName() + ".")); return null; } - return spaceId; + return new Selection(spaceId, spaceRef); } - SpaceId firstSpaceId = firstRegisteredSpaceId(resource); + SpaceId firstSpaceId = PhysicsSpaces.spaceIds(physicsStore) + .stream() + .min(Comparator.comparingInt(SpaceId::value)) + .orElse(null); if (firstSpaceId == null) { context.sendMessage(Message.raw("No physics space exists. Run " + "`/impulse space create --backend=` before targeting space settings.")); + return null; } - return firstSpaceId; - } - - @Nullable - static SpaceId specifiedSpaceId(@Nonnull PhysicsWorldResource resource, int rawSpaceId) { - if (rawSpaceId <= 0) { + Ref spaceRef = PhysicsSpaces.resolveRef(physicsStore, firstSpaceId); + if (spaceRef == null) { + context.sendMessage(Message.raw("No physics space id=" + firstSpaceId.value() + + " exists in world " + world.getName() + ".")); return null; } - SpaceId spaceId = new SpaceId(rawSpaceId); - return resource.hasSpace(spaceId) ? spaceId : null; + return new Selection(firstSpaceId, spaceRef); } - @Nullable - static SpaceId firstRegisteredSpaceId(@Nonnull PhysicsWorldResource resource) { - return resource.getSpaceIds() - .stream() - .min(Comparator.comparingInt(SpaceId::value)) - .orElse(null); + record Selection(@Nonnull SpaceId spaceId, @Nonnull Ref spaceRef) { } } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/components/ChunkCollisionRestoreDependencyComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/components/ChunkCollisionRestoreDependencyComponent.java new file mode 100644 index 00000000..0c785db3 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/components/ChunkCollisionRestoreDependencyComponent.java @@ -0,0 +1,102 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.codec.codecs.EnumCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.math.vector.Vector3fUtil; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import lombok.Getter; +import org.joml.Vector3f; + +/** + * Utility component to recover physicschunk bodies when loading bodies on top + */ +public final class ChunkCollisionRestoreDependencyComponent implements Component { + + @Nonnull + public static final BuilderCodec CODEC = + BuilderCodec.builder(ChunkCollisionRestoreDependencyComponent.class, + ChunkCollisionRestoreDependencyComponent::new) + .append(new KeyedCodec<>("SpaceUuid", Codec.UUID_BINARY, false), + (component, value) -> component.spaceUuid = value != null + ? value + : new UUID(0L, 0L), + ChunkCollisionRestoreDependencyComponent::getSpaceUuid) + .add() + .append(new KeyedCodec<>("Center", Vector3fUtil.CODEC, false), + (component, value) -> component.center.set(value != null ? value : new Vector3f()), + ChunkCollisionRestoreDependencyComponent::getCenter) + .add() + .append(new KeyedCodec<>("Radius", Codec.INTEGER, false), + (component, value) -> component.radius = Math.max(0, value != null ? value : 0), + ChunkCollisionRestoreDependencyComponent::getRadius) + .add() + .append(new KeyedCodec<>("ModeAtSave", + new EnumCodec<>(PhysicsChunkCollisionMode.class), + false), + (component, value) -> component.modeAtSave = value != null + ? value + : PhysicsChunkCollisionMode.NONE, + ChunkCollisionRestoreDependencyComponent::getModeAtSave) + .add() + .build(); + + @Nonnull + private UUID spaceUuid = new UUID(0L, 0L); + @Nonnull + private final Vector3f center = new Vector3f(); + @Getter + private int radius; + @Nonnull + private PhysicsChunkCollisionMode modeAtSave = PhysicsChunkCollisionMode.NONE; + + public ChunkCollisionRestoreDependencyComponent() { + } + + public ChunkCollisionRestoreDependencyComponent(@Nonnull UUID spaceUuid, + @Nonnull Vector3f center, + int radius, + @Nonnull PhysicsChunkCollisionMode modeAtSave) { + this.spaceUuid = Objects.requireNonNull(spaceUuid, "spaceUuid"); + this.center.set(Objects.requireNonNull(center, "center")); + this.radius = Math.max(0, radius); + this.modeAtSave = Objects.requireNonNull(modeAtSave, "modeAtSave"); + } + + @Nonnull + public UUID getSpaceUuid() { + return spaceUuid; + } + + @Nonnull + public Vector3f getCenter() { + return new Vector3f(center); + } + + @Nonnull + public PhysicsChunkCollisionMode getModeAtSave() { + return modeAtSave; + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypeRegistry.chunkCollisionRestoreDependencyComponentType(); + } + + @Nonnull + @Override + public ChunkCollisionRestoreDependencyComponent clone() { + return new ChunkCollisionRestoreDependencyComponent(spaceUuid, + center, + radius, + modeAtSave); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/components/ChunkCollisionSourceComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/components/ChunkCollisionSourceComponent.java new file mode 100644 index 00000000..862ac489 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/components/ChunkCollisionSourceComponent.java @@ -0,0 +1,128 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.codec.codecs.EnumCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import lombok.Getter; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * Internal PhysicsChunk source metadata for generated chunk collision body rows. + */ +public final class ChunkCollisionSourceComponent implements Component { + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + ChunkCollisionSourceComponent.class, + ChunkCollisionSourceComponent::new) + .append(new KeyedCodec<>("SourceKey", Codec.STRING, false), + (component, value) -> component.sourceKey = value != null ? value : "", + ChunkCollisionSourceComponent::getSourceKey) + .add() + .append(new KeyedCodec<>("ChunkX", Codec.INTEGER, false), + (component, value) -> component.chunkX = value != null ? value : 0, + ChunkCollisionSourceComponent::getChunkX) + .add() + .append(new KeyedCodec<>("SectionY", Codec.INTEGER, false), + (component, value) -> component.sectionY = value != null ? value : 0, + ChunkCollisionSourceComponent::getSectionY) + .add() + .append(new KeyedCodec<>("ChunkZ", Codec.INTEGER, false), + (component, value) -> component.chunkZ = value != null ? value : 0, + ChunkCollisionSourceComponent::getChunkZ) + .add() + .append(new KeyedCodec<>("PayloadResourceKey", Codec.STRING, false), + (component, value) -> component.payloadResourceKey = value != null ? value : "", + ChunkCollisionSourceComponent::getPayloadResourceKey) + .add() + .append(new KeyedCodec<>("PartKind", new EnumCodec<>(PartKind.class), false), + (component, value) -> component.partKind = value != null + ? value + : PartKind.BOX, + ChunkCollisionSourceComponent::getPartKind) + .add() + .append(new KeyedCodec<>("PartIndex", Codec.INTEGER, false), + (component, value) -> component.partIndex = value != null ? value : 0, + ChunkCollisionSourceComponent::getPartIndex) + .add() + .build(); + + @Nonnull + private String sourceKey = ""; + @Getter + private int chunkX; + @Getter + private int sectionY; + @Getter + private int chunkZ; + @Nonnull + private String payloadResourceKey = ""; + @Nonnull + private PartKind partKind = PartKind.BOX; + @Getter + private int partIndex; + + public ChunkCollisionSourceComponent() { + } + + public ChunkCollisionSourceComponent(@Nonnull String sourceKey, + int chunkX, + int sectionY, + int chunkZ, + @Nonnull String payloadResourceKey, + @Nonnull PartKind partKind, + int partIndex) { + this.sourceKey = Objects.requireNonNull(sourceKey, "sourceKey"); + this.chunkX = chunkX; + this.sectionY = sectionY; + this.chunkZ = chunkZ; + this.payloadResourceKey = Objects.requireNonNull(payloadResourceKey, + "payloadResourceKey"); + this.partKind = Objects.requireNonNull(partKind, "partKind"); + this.partIndex = partIndex; + } + + @Nonnull + public String getSourceKey() { + return sourceKey; + } + + @Nonnull + public String getPayloadResourceKey() { + return payloadResourceKey; + } + + @Nonnull + public PartKind getPartKind() { + return partKind; + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypeRegistry.chunkCollisionSourceComponentType(); + } + + @Nonnull + @Override + public ChunkCollisionSourceComponent clone() { + return new ChunkCollisionSourceComponent(sourceKey, + chunkX, + sectionY, + chunkZ, + payloadResourceKey, + partKind, + partIndex); + } + + public enum PartKind { + BOX, + DETAIL_BOX, + NATIVE_VOXELS + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/profiling/WorldCollisionProfilingResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/profiling/PhysicsChunkProfilingResource.java similarity index 92% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/profiling/WorldCollisionProfilingResource.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/profiling/PhysicsChunkProfilingResource.java index cb1d519c..23678388 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/profiling/WorldCollisionProfilingResource.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/profiling/PhysicsChunkProfilingResource.java @@ -1,16 +1,16 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling; +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.profiling; import com.hypixel.hytale.component.Resource; import com.hypixel.hytale.component.ResourceType; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldVoxelCollisionCache.BuildStats; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkBuildStats; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import it.unimi.dsi.fastutil.longs.LongSet; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.UUID; import javax.annotation.Nonnull; import javax.annotation.Nullable; import lombok.AccessLevel; @@ -20,7 +20,7 @@ import org.joml.Vector3f; /** - * Runtime-only profiling state for world collision streaming. + * Runtime-only profiling state for PhysicsChunk collision streaming. * *

This resource collects targeted metrics for the streamed voxel-collision * path so performance work can be driven by section/build/prune behavior rather @@ -28,7 +28,7 @@ * part of persisted physics world state.

*/ @Getter -public class WorldCollisionProfilingResource implements Resource { +public class PhysicsChunkProfilingResource implements Resource { @Setter private boolean enabled; @@ -42,13 +42,13 @@ public class WorldCollisionProfilingResource implements Resource { private RetainedSectionEnvelope diagnosticRetainedEnvelope; @Nullable - private static ResourceType resourceType; + private static ResourceType resourceType; - public WorldCollisionProfilingResource() { + public PhysicsChunkProfilingResource() { } public static void setResourceType( - @Nonnull ResourceType type) { + @Nonnull ResourceType type) { resourceType = Objects.requireNonNull(type, "type"); } @@ -113,8 +113,8 @@ public synchronized Snapshot getWorstTickSnapshot() { @Nonnull @Override - public synchronized WorldCollisionProfilingResource clone() { - WorldCollisionProfilingResource copy = new WorldCollisionProfilingResource(); + public synchronized PhysicsChunkProfilingResource clone() { + PhysicsChunkProfilingResource copy = new PhysicsChunkProfilingResource(); copy.enabled = enabled; copy.cumulative.copyFrom(cumulative); copy.latestTick.copyFrom(latestTick); @@ -124,9 +124,9 @@ public synchronized WorldCollisionProfilingResource clone() { } @Nonnull - public static ResourceType getResourceType() { + public static ResourceType getResourceType() { if (resourceType == null) { - throw new IllegalStateException("World collision profiling resource is not registered"); + throw new IllegalStateException("PhysicsChunk profiling resource is not registered"); } return resourceType; } @@ -155,8 +155,8 @@ public static final class Snapshot { private int playerSectionTargets; private int bodySectionTargets; private int streamingSpaces; - private int terrainApplyQueued; - private int terrainApplySkippedPending; + private int collisionApplyQueued; + private int collisionApplySkippedPending; private int ensureCalls; private int sectionRequests; private int sectionCacheHits; @@ -207,12 +207,12 @@ public void incrementStreamingSpaces() { streamingSpaces++; } - public void incrementTerrainApplyQueued() { - terrainApplyQueued++; + public void incrementCollisionApplyQueued() { + collisionApplyQueued++; } - public void incrementTerrainApplySkippedPending() { - terrainApplySkippedPending++; + public void incrementCollisionApplySkippedPending() { + collisionApplySkippedPending++; } public void addBodyStreamingCandidates(int count) { @@ -332,7 +332,7 @@ public void incrementDuplicateSkips() { duplicateSkips++; } - public void addBuildStats(@Nonnull BuildStats stats) { + public void addBuildStats(@Nonnull PhysicsChunkBuildStats stats) { scannedBlocks += stats.scannedBlocks(); solidBlocks += stats.solidBlocks(); culledInteriorBlocks += stats.culledInteriorBlocks(); @@ -400,8 +400,8 @@ public void copyFrom(@Nonnull Snapshot other) { playerSectionTargets = other.playerSectionTargets; bodySectionTargets = other.bodySectionTargets; streamingSpaces = other.streamingSpaces; - terrainApplyQueued = other.terrainApplyQueued; - terrainApplySkippedPending = other.terrainApplySkippedPending; + collisionApplyQueued = other.collisionApplyQueued; + collisionApplySkippedPending = other.collisionApplySkippedPending; ensureCalls = other.ensureCalls; sectionRequests = other.sectionRequests; sectionCacheHits = other.sectionCacheHits; @@ -459,8 +459,8 @@ public void add(@Nonnull Snapshot other) { playerSectionTargets += other.playerSectionTargets; bodySectionTargets += other.bodySectionTargets; streamingSpaces += other.streamingSpaces; - terrainApplyQueued += other.terrainApplyQueued; - terrainApplySkippedPending += other.terrainApplySkippedPending; + collisionApplyQueued += other.collisionApplyQueued; + collisionApplySkippedPending += other.collisionApplySkippedPending; ensureCalls += other.ensureCalls; sectionRequests += other.sectionRequests; sectionCacheHits += other.sectionCacheHits; @@ -516,8 +516,8 @@ public void reset() { playerSectionTargets = 0; bodySectionTargets = 0; streamingSpaces = 0; - terrainApplyQueued = 0; - terrainApplySkippedPending = 0; + collisionApplyQueued = 0; + collisionApplySkippedPending = 0; ensureCalls = 0; sectionRequests = 0; sectionCacheHits = 0; @@ -634,7 +634,7 @@ public String compact() { } public record StreamingTargetDiagnostic(@Nonnull StreamingTargetType targetType, - @Nullable RigidBodyKey bodyKey, + @Nullable UUID bodyUuid, @Nullable DiagnosticPosition snapshotPosition, @Nullable DiagnosticPosition livePosition) { @@ -648,17 +648,17 @@ public static StreamingTargetDiagnostic player(@Nonnull Vector3d position) { } @Nonnull - public static StreamingTargetDiagnostic body(@Nonnull RigidBodyKey bodyKey, + public static StreamingTargetDiagnostic body(@Nonnull UUID bodyUuid, @Nonnull Vector3f snapshotPosition, @Nonnull Vector3f livePosition) { return new StreamingTargetDiagnostic(StreamingTargetType.BODY, - bodyKey, + bodyUuid, DiagnosticPosition.from(snapshotPosition), DiagnosticPosition.from(livePosition)); } @Nonnull - public static StreamingTargetDiagnostic body(@Nonnull RigidBodyKey bodyKey, + public static StreamingTargetDiagnostic body(@Nonnull UUID bodyUuid, float snapshotX, float snapshotY, float snapshotZ, @@ -666,7 +666,7 @@ public static StreamingTargetDiagnostic body(@Nonnull RigidBodyKey bodyKey, float liveY, float liveZ) { return new StreamingTargetDiagnostic(StreamingTargetType.BODY, - bodyKey, + bodyUuid, DiagnosticPosition.from(snapshotX, snapshotY, snapshotZ), DiagnosticPosition.from(liveX, liveY, liveZ)); } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/resources/PhysicsChunkCollisionMutationQueueResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/resources/PhysicsChunkCollisionMutationQueueResource.java new file mode 100644 index 00000000..e1114793 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/resources/PhysicsChunkCollisionMutationQueueResource.java @@ -0,0 +1,93 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources; + +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionMutation; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkLifecycle; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Objects; +import java.util.function.Predicate; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Copied chunk collision mutation queue drained by PhysicsStore.tick(). + */ +public final class PhysicsChunkCollisionMutationQueueResource implements Resource { + + @Nullable + private static ResourceType resourceType; + @Nonnull + private final LinkedHashMap mutations = + new LinkedHashMap<>(); + private long lifecycleGeneration = PhysicsChunkLifecycle.generation(); + private long settingsGeneration = PhysicsChunkSettingsIndexResource.INITIAL_GENERATION; + + public PhysicsChunkCollisionMutationQueueResource() { + } + + public synchronized void enqueue(@Nonnull ChunkCollisionMutation mutation) { + ChunkCollisionMutation stamped = Objects.requireNonNull(mutation, "mutation") + .stamped(lifecycleGeneration, settingsGeneration); + MutationKey key = new MutationKey(stamped.spaceUuid(), stamped.sourceKey()); + mutations.remove(key); + mutations.put(key, stamped); + } + + public synchronized void updateStamp(long lifecycleGeneration, long settingsGeneration) { + this.lifecycleGeneration = lifecycleGeneration; + this.settingsGeneration = settingsGeneration; + } + + @Nonnull + public synchronized List drain() { + List drained = new ArrayList<>(mutations.values()); + mutations.clear(); + return drained; + } + + public synchronized int size() { + return mutations.size(); + } + + public synchronized int removeIf(@Nonnull Predicate predicate) { + Objects.requireNonNull(predicate, "predicate"); + int before = mutations.size(); + mutations.entrySet().removeIf(entry -> predicate.test(entry.getValue())); + return before - mutations.size(); + } + + public synchronized void clear() { + mutations.clear(); + } + + @Nonnull + @Override + public synchronized PhysicsChunkCollisionMutationQueueResource clone() { + PhysicsChunkCollisionMutationQueueResource copy = new PhysicsChunkCollisionMutationQueueResource(); + copy.mutations.putAll(mutations); + copy.lifecycleGeneration = lifecycleGeneration; + copy.settingsGeneration = settingsGeneration; + return copy; + } + + @Nonnull + public static ResourceType getResourceType() { + return resourceType; + } + + public static void setResourceType( + @Nonnull ResourceType type) { + resourceType = type; + } + + public static void clearResourceType() { + resourceType = null; + } + + private record MutationKey(@Nonnull java.util.UUID spaceUuid, @Nonnull String sourceKey) { + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/resources/PhysicsChunkCollisionPayloadResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/resources/PhysicsChunkCollisionPayloadResource.java new file mode 100644 index 00000000..67fb6847 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/resources/PhysicsChunkCollisionPayloadResource.java @@ -0,0 +1,64 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources; + +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionPayload; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Runtime-only copied PhysicsChunk collision payloads keyed by ChunkCollisionSourceComponent payload keys. + */ +public final class PhysicsChunkCollisionPayloadResource implements Resource { + + @Nullable + private static ResourceType resourceType; + @Nonnull + private final Map payloadsByKey = + new Object2ObjectOpenHashMap<>(); + + public PhysicsChunkCollisionPayloadResource() { + } + + public void put(@Nonnull String key, @Nonnull ChunkCollisionPayload payload) { + payloadsByKey.put(key, payload); + } + + @Nullable + public ChunkCollisionPayload get(@Nonnull String key) { + return payloadsByKey.get(key); + } + + public void remove(@Nonnull String key) { + payloadsByKey.remove(key); + } + + public void clear() { + payloadsByKey.clear(); + } + + @Nonnull + @Override + public PhysicsChunkCollisionPayloadResource clone() { + PhysicsChunkCollisionPayloadResource copy = new PhysicsChunkCollisionPayloadResource(); + copy.payloadsByKey.putAll(payloadsByKey); + return copy; + } + + @Nonnull + public static ResourceType getResourceType() { + return resourceType; + } + + public static void setResourceType( + @Nonnull ResourceType type) { + resourceType = type; + } + + public static void clearResourceType() { + resourceType = null; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/resources/PhysicsChunkComponentSyncResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/resources/PhysicsChunkComponentSyncResource.java new file mode 100644 index 00000000..8306e3e9 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/resources/PhysicsChunkComponentSyncResource.java @@ -0,0 +1,103 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources; + +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkCollisionDefaults; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Tracks space-level components that generated PhysicsChunk body rows inherit at runtime. + */ +public final class PhysicsChunkComponentSyncResource implements Resource { + + @Nullable + private static ResourceType resourceType; + @Nonnull + private final Map surfacesBySpaceUuid = + new Object2ObjectOpenHashMap<>(); + + public PhysicsChunkComponentSyncResource() { + } + + @Nonnull + public synchronized Set replaceAll( + @Nonnull Map surfaces) { + Set changed = new ObjectOpenHashSet<>(); + for (Map.Entry entry : surfaces.entrySet()) { + ChunkCollisionSurfaceComponents previous = surfacesBySpaceUuid.get(entry.getKey()); + if (!entry.getValue().equals(previous)) { + changed.add(entry.getKey()); + } + } + surfacesBySpaceUuid.clear(); + surfacesBySpaceUuid.putAll(surfaces); + return changed; + } + + public synchronized void clear() { + surfacesBySpaceUuid.clear(); + } + + @Nonnull + @Override + public synchronized PhysicsChunkComponentSyncResource clone() { + PhysicsChunkComponentSyncResource copy = new PhysicsChunkComponentSyncResource(); + copy.surfacesBySpaceUuid.putAll(surfacesBySpaceUuid); + return copy; + } + + @Nonnull + public static ResourceType getResourceType() { + return resourceType; + } + + public static void setResourceType( + @Nonnull ResourceType type) { + resourceType = type; + } + + public static void clearResourceType() { + resourceType = null; + } + + public record ChunkCollisionSurfaceComponents(float friction, + float restitution, + int collisionGroup, + int collisionMask) { + + @Nonnull + public static ChunkCollisionSurfaceComponents of(@Nullable MaterialComponent material, + @Nullable CollisionFilterComponent filter) { + return new ChunkCollisionSurfaceComponents( + material != null ? material.getFriction() : PhysicsChunkCollisionDefaults.FRICTION, + material != null + ? material.getRestitution() + : PhysicsChunkCollisionDefaults.RESTITUTION, + filter != null + ? filter.getCollisionGroup() + : PhysicsChunkCollisionDefaults.COLLISION_GROUP, + filter != null + ? filter.getCollisionMask() + : PhysicsChunkCollisionDefaults.COLLISION_MASK); + } + + @Nonnull + public MaterialComponent material() { + return new MaterialComponent(friction, restitution); + } + + @Nonnull + public CollisionFilterComponent filter() { + return new CollisionFilterComponent(collisionGroup, collisionMask); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/resources/PhysicsChunkSettingsIndexResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/resources/PhysicsChunkSettingsIndexResource.java new file mode 100644 index 00000000..aed4f9d1 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/resources/PhysicsChunkSettingsIndexResource.java @@ -0,0 +1,103 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources; + +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkBuildOptions; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.EntityChunkBoundaryMode; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Runtime-only copied PhysicsChunk settings indexed by PhysicsStore space UUID. + */ +public final class PhysicsChunkSettingsIndexResource implements Resource { + + public static final long INITIAL_GENERATION = 1L; + + @Nullable + private static ResourceType resourceType; + @Nonnull + private final Map settingsBySpaceUuid = + new Object2ObjectOpenHashMap<>(); + private long generation = INITIAL_GENERATION; + + public PhysicsChunkSettingsIndexResource() { + } + + public synchronized void replaceAll(@Nonnull Map settings) { + if (settingsBySpaceUuid.equals(settings)) { + return; + } + settingsBySpaceUuid.clear(); + settingsBySpaceUuid.putAll(settings); + generation++; + } + + @Nonnull + public synchronized List streamingSpaces() { + return settingsBySpaceUuid.values().stream() + .filter(settings -> settings.mode() == PhysicsChunkCollisionMode.STREAMING) + .toList(); + } + + @Nullable + public synchronized PhysicsChunkSpaceSettings settings(@Nonnull UUID spaceUuid) { + return settingsBySpaceUuid.get(spaceUuid); + } + + public synchronized long generation() { + return generation; + } + + public synchronized void clear() { + if (!settingsBySpaceUuid.isEmpty()) { + generation++; + } + settingsBySpaceUuid.clear(); + } + + @Nonnull + @Override + public synchronized PhysicsChunkSettingsIndexResource clone() { + PhysicsChunkSettingsIndexResource copy = new PhysicsChunkSettingsIndexResource(); + copy.settingsBySpaceUuid.putAll(settingsBySpaceUuid); + copy.generation = generation; + return copy; + } + + @Nonnull + public static ResourceType getResourceType() { + return resourceType; + } + + public static void setResourceType( + @Nonnull ResourceType type) { + resourceType = type; + } + + public static void clearResourceType() { + resourceType = null; + } + + public record PhysicsChunkSpaceSettings(@Nonnull UUID spaceUuid, + @Nonnull PhysicsChunkCollisionMode mode, + @Nonnull EntityChunkBoundaryMode entityChunkBoundaryMode, + boolean nativeVoxelCollisionEnabled, + int radius, + int bodyRadius, + int ttlTicks) { + + @Nonnull + public PhysicsChunkBuildOptions buildOptions() { + return new PhysicsChunkBuildOptions( + ChunkCollisionMode.fromNativeVoxelCollisionEnabled(nativeVoxelCollisionEnabled)); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionComponentSyncSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionComponentSyncSystem.java new file mode 100644 index 00000000..7eebfc5d --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionComponentSyncSystem.java @@ -0,0 +1,195 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.QuerySystem; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkComponentSyncResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkComponentSyncResource.ChunkCollisionSurfaceComponents; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.systems.IdentityIndexSystem; +import dev.hytalemodding.impulse.core.internal.systems.PhysicsStoreSystemSupport; +import dev.hytalemodding.impulse.core.internal.systems.binding.BodyBindingSystem; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; + +/** + * Propagates space-row material and collision-filter component changes to generated chunk bodies. + */ +public final class ChunkCollisionComponentSyncSystem extends TickingSystem + implements QuerySystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, IdentityIndexSystem.class), + new SystemDependency<>(Order.AFTER, ChunkCollisionMutationDrainSystem.class), + new SystemDependency<>(Order.BEFORE, BodyBindingSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + Map surfaces = collectSpaceSurfaces(store, + systemIndex); + Set changedSpaces = store + .getResource(PhysicsChunkComponentSyncResource.getResourceType()) + .replaceAll(surfaces); + if (changedSpaces.isEmpty()) { + return; + } + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + BiConsumer, CommandBuffer> collector = + (chunk, commandBuffer) -> syncChunk(runtime, + changedSpaces, + surfaces, + chunk, + commandBuffer); + store.forEachChunk(systemIndex, collector); + } + + @Nonnull + private static Map collectSpaceSurfaces( + @Nonnull Store store, + int systemIndex) { + Map surfaces = new Object2ObjectOpenHashMap<>(); + BiConsumer, CommandBuffer> collector = + (chunk, _) -> collectSpaceSurfaces(surfaces, chunk); + store.forEachChunk(systemIndex, collector); + return surfaces; + } + + private static void collectSpaceSurfaces( + @Nonnull Map surfaces, + @Nonnull ArchetypeChunk chunk) { + for (int index = 0; index < chunk.size(); index++) { + if (chunk.getComponent(index, SpaceComponent.getComponentType()) == null) { + continue; + } + UUID spaceUuid = PhysicsStoreSystemSupport.rowUuid(chunk, index); + if (PhysicsStoreSystemSupport.isNil(spaceUuid)) { + continue; + } + surfaces.put(spaceUuid, + ChunkCollisionSurfaceComponents.of( + chunk.getComponent(index, MaterialComponent.getComponentType()), + chunk.getComponent(index, CollisionFilterComponent.getComponentType()))); + } + } + + private static void syncChunk(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull Set changedSpaces, + @Nonnull Map surfaces, + @Nonnull ArchetypeChunk chunk, + @Nonnull CommandBuffer commandBuffer) { + for (int index = 0; index < chunk.size(); index++) { + if (chunk.getComponent(index, ChunkCollisionSourceComponent.getComponentType()) == null) { + continue; + } + BodyComponent body = chunk.getComponent(index, BodyComponent.getComponentType()); + if (body == null || !changedSpaces.contains(body.getSpaceUuid())) { + continue; + } + ChunkCollisionSurfaceComponents surface = surfaces.get(body.getSpaceUuid()); + if (surface == null) { + continue; + } + Ref bodyRef = chunk.getReferenceTo(index); + boolean materialChanged = syncMaterial(commandBuffer, bodyRef, surface, + chunk.getComponent(index, MaterialComponent.getComponentType())); + boolean filterChanged = syncFilter(commandBuffer, bodyRef, surface, + chunk.getComponent(index, CollisionFilterComponent.getComponentType())); + if (materialChanged || filterChanged) { + syncBackend(runtime, bodyRef, surface, materialChanged, filterChanged); + } + } + } + + private static boolean syncMaterial(@Nonnull CommandBuffer commandBuffer, + @Nonnull Ref bodyRef, + @Nonnull ChunkCollisionSurfaceComponents surface, + MaterialComponent material) { + if (material != null + && Float.compare(material.getFriction(), surface.friction()) == 0 + && Float.compare(material.getRestitution(), surface.restitution()) == 0) { + return false; + } + commandBuffer.putComponent(bodyRef, + MaterialComponent.getComponentType(), + surface.material()); + return true; + } + + private static boolean syncFilter(@Nonnull CommandBuffer commandBuffer, + @Nonnull Ref bodyRef, + @Nonnull ChunkCollisionSurfaceComponents surface, + CollisionFilterComponent filter) { + if (filter != null + && filter.getCollisionGroup() == surface.collisionGroup() + && filter.getCollisionMask() == surface.collisionMask()) { + return false; + } + commandBuffer.putComponent(bodyRef, + CollisionFilterComponent.getComponentType(), + surface.filter()); + return true; + } + + private static void syncBackend(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull Ref bodyRef, + @Nonnull ChunkCollisionSurfaceComponents surface, + boolean materialChanged, + boolean filterChanged) { + BackendBodyHandle bodyHandle = runtime.getBodyHandle(bodyRef); + BackendSpaceHandle spaceHandle = runtime.getBodySpaceHandle(bodyRef); + if (bodyHandle == null || spaceHandle == null) { + return; + } + PhysicsBackendRuntime backendRuntime = runtime.runtimeForBodyRef(bodyRef); + if (backendRuntime == null) { + return; + } + if (materialChanged) { + backendRuntime.setBodyFriction(spaceHandle.value(), + bodyHandle.value(), + surface.friction()); + backendRuntime.setBodyRestitution(spaceHandle.value(), + bodyHandle.value(), + surface.restitution()); + } + if (filterChanged) { + backendRuntime.setBodyCollisionFilter(spaceHandle.value(), + bodyHandle.value(), + surface.collisionGroup(), + surface.collisionMask()); + } + } + + @Nonnull + @Override + public Query getQuery() { + return PhysicsStoreSystemSupport.uuidQuery(); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionMutationDrainSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionMutationDrainSystem.java new file mode 100644 index 00000000..0b40edb1 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionMutationDrainSystem.java @@ -0,0 +1,482 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.math.util.ChunkUtil; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsAxis; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionMutationQueueResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionPayloadResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkSettingsIndexResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionMutation; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionPayload; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionPayload.BoxPayload; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkLifecycle; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkCollisionDefaults; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsStoreRowCleanup; +import dev.hytalemodding.impulse.core.internal.systems.PhysicsStoreSystemSupport; +import dev.hytalemodding.impulse.core.internal.systems.SpaceSettingsApplicationSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.BodyBindingSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.SpaceBindingSystem; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent.PartKind; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +/** + * Applies copied PhysicsChunk chunk collision mutations as chunk collision body rows. + */ +public final class ChunkCollisionMutationDrainSystem extends TickingSystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, SpaceBindingSystem.class), + new SystemDependency<>(Order.AFTER, SpaceSettingsApplicationSystem.class), + new SystemDependency<>(Order.AFTER, PhysicsChunkSettingsIndexSystem.class), + new SystemDependency<>(Order.BEFORE, BodyBindingSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + if (restore.isFailed()) { + return; + } + PhysicsChunkCollisionMutationQueueResource queue = store.getResource( + PhysicsChunkCollisionMutationQueueResource.getResourceType()); + List mutations = coalesceLastMutationPerSource(queue.drain()); + if (mutations.isEmpty()) { + return; + } + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + PhysicsChunkCollisionPayloadResource chunkCollisionPayloads = store.getResource( + PhysicsChunkCollisionPayloadResource.getResourceType()); + PhysicsChunkSettingsIndexResource settingsIndex = store.getResource( + PhysicsChunkSettingsIndexResource.getResourceType()); + List upserts = prepareUpserts(store, + runtime, + settingsIndex, + restore, + mutations); + + removeGeneratedRows(store, + runtime, + removalKeys(mutations, upserts)); + applyPreparedUpserts(store, + chunkCollisionPayloads, + upserts); + } + + @Nonnull + private static List coalesceLastMutationPerSource( + @Nonnull List mutations) { + Map latest = new LinkedHashMap<>(); + for (ChunkCollisionMutation mutation : mutations) { + MutationKey key = new MutationKey(mutation.spaceUuid(), mutation.sourceKey()); + latest.remove(key); + latest.put(key, mutation); + } + return new ArrayList<>(latest.values()); + } + + @Nonnull + private static Map> removalKeys( + @Nonnull List mutations, + @Nonnull List upserts) { + Map> keys = new Object2ObjectOpenHashMap<>(); + for (ChunkCollisionMutation mutation : mutations) { + if (mutation.remove()) { + addRemovalKey(keys, mutation); + } + } + for (PreparedUpsert upsert : upserts) { + addRemovalKey(keys, upsert.mutation()); + } + return keys; + } + + private static void addRemovalKey(@Nonnull Map> keys, + @Nonnull ChunkCollisionMutation mutation) { + keys.computeIfAbsent(mutation.spaceUuid(), _ -> new ObjectOpenHashSet<>()) + .add(mutation.sourceKey()); + } + + @Nonnull + private static List prepareUpserts(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsChunkSettingsIndexResource settingsIndex, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull List mutations) { + List upserts = new ArrayList<>(); + for (ChunkCollisionMutation mutation : mutations) { + if (!mutation.remove() && isFreshUpsert(settingsIndex, mutation)) { + PreparedUpsert upsert = prepareUpsert(store, runtime, restore, mutation); + if (upsert != null) { + upserts.add(upsert); + } + } + } + return upserts; + } + + private static boolean isFreshUpsert(@Nonnull PhysicsChunkSettingsIndexResource settingsIndex, + @Nonnull ChunkCollisionMutation mutation) { + return mutation.lifecycleGeneration() == PhysicsChunkLifecycle.generation() + && mutation.settingsGeneration() == settingsIndex.generation() + && settingsIndex.settings(mutation.spaceUuid()) != null; + } + + @Nullable + private static PreparedUpsert prepareUpsert(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull ChunkCollisionMutation mutation) { + ChunkCollisionPayload payload = mutation.payload(); + if (payload == null || payload.isEmpty()) { + restore.recordSoftSkip("Chunk collision upsert payload is missing: " + + mutation.sourceKey()); + return null; + } + Ref spaceRef = store.getExternalData().getRefFromUUID(mutation.spaceUuid()); + if (spaceRef != null && (spaceRef.getStore() != store || !spaceRef.isValid())) { + spaceRef = null; + } + BackendSpaceHandle spaceHandle = spaceRef != null ? runtime.getSpaceHandle(spaceRef) : null; + boolean nativeVoxel; + try (PhysicsBackendRuntime backendRuntime = spaceRef != null + ? runtime.runtimeForSpaceRef(spaceRef) + : null) { + if (spaceRef == null || spaceHandle == null || backendRuntime == null) { + restore.recordSoftSkip("Chunk collision references unbound space: " + + mutation.sourceKey()); + return null; + } + nativeVoxel = payload.nativeVoxelCollisionEnabled() + && payload.hasFullCubeVoxels() + && backendRuntime.supportsVoxelTerrain(spaceHandle.value()); + } + MaterialComponent material = material(store, spaceRef); + CollisionFilterComponent filter = filter(store, spaceRef); + return new PreparedUpsert(mutation, payload, spaceRef, material, filter, nativeVoxel); + } + + private static void applyPreparedUpserts(@Nonnull Store store, + @Nonnull PhysicsChunkCollisionPayloadResource chunkCollisionPayloads, + @Nonnull List upserts) { + for (PreparedUpsert upsert : upserts) { + applyPreparedUpsert(store, chunkCollisionPayloads, upsert); + } + } + + private static void applyPreparedUpsert(@Nonnull Store store, + @Nonnull PhysicsChunkCollisionPayloadResource chunkCollisionPayloads, + @Nonnull PreparedUpsert upsert) { + ChunkCollisionMutation mutation = upsert.mutation(); + ChunkCollisionPayload payload = upsert.payload(); + removePayload(chunkCollisionPayloads, mutation.payloadResourceKey()); + if (upsert.nativeVoxel()) { + chunkCollisionPayloads.put(mutation.payloadResourceKey(), voxelPayload(payload)); + addNativeVoxelBody(store, + upsert.spaceRef(), + mutation, + upsert.material(), + upsert.filter()); + } else { + addBoxBodies(store, + upsert.spaceRef(), + mutation, + upsert.material(), + upsert.filter(), + payload.mergedFullCubeBoxes(), + PartKind.BOX); + } + addBoxBodies(store, + upsert.spaceRef(), + mutation, + upsert.material(), + upsert.filter(), + payload.detailBoxes(), + PartKind.DETAIL_BOX); + } + + @Nonnull + private static ChunkCollisionPayload voxelPayload(@Nonnull ChunkCollisionPayload payload) { + return new ChunkCollisionPayload(payload.voxelSizeX(), + payload.voxelSizeY(), + payload.voxelSizeZ(), + payload.voxelCoordinates(), + List.of(), + List.of(), + true, + payload.neighbors()); + } + + private static void addNativeVoxelBody(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull ChunkCollisionMutation mutation, + @Nonnull MaterialComponent material, + @Nonnull CollisionFilterComponent filter) { + TargetComponent target = new TargetComponent(); + target.setPosition(new Vector3f(mutation.chunkX() << ChunkUtil.BITS, + mutation.sectionY() << ChunkUtil.BITS, + mutation.chunkZ() << ChunkUtil.BITS)); + addChunkCollisionBody(store, + spaceRef, + mutation, + target, + new ShapeComponent(ShapeType.VOXELS, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + PhysicsAxis.Y, + 0.0f, + mutation.payloadResourceKey()), + material, + filter, + mutation.payloadResourceKey(), + PartKind.NATIVE_VOXELS, + 0); + } + + private static void addBoxBodies(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull ChunkCollisionMutation mutation, + @Nonnull MaterialComponent material, + @Nonnull CollisionFilterComponent filter, + @Nonnull List boxes, + @Nonnull PartKind partKind) { + for (int index = 0; index < boxes.size(); index++) { + BoxPayload box = boxes.get(index); + if (box.halfX() <= 0.0 || box.halfY() <= 0.0 || box.halfZ() <= 0.0) { + continue; + } + TargetComponent target = new TargetComponent(); + target.setPosition(new Vector3f((float) box.centerX(), + (float) box.centerY(), + (float) box.centerZ())); + addChunkCollisionBody(store, + spaceRef, + mutation, + target, + new ShapeComponent(ShapeType.BOX, + (float) box.halfX(), + (float) box.halfY(), + (float) box.halfZ(), + 0.0f, + 0.0f, + PhysicsAxis.Y, + 0.0f, + ""), + material, + filter, + "", + partKind, + index); + } + } + + private static void addChunkCollisionBody(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull ChunkCollisionMutation mutation, + @Nonnull TargetComponent target, + @Nonnull ShapeComponent shape, + @Nonnull MaterialComponent material, + @Nonnull CollisionFilterComponent filter, + @Nonnull String payloadResourceKey, + @Nonnull PartKind partKind, + int partIndex) { + UUID bodyUuid = chunkCollisionBodyUuid(mutation.spaceUuid(), + mutation.sourceKey(), + partKind, + partIndex); + BodyComponent body = new BodyComponent(mutation.spaceUuid()); + body.setSpaceRef(spaceRef); + var holder = PhysicsEntities.bodyHolder(store, + bodyUuid, + body, + new DynamicsComponent(PhysicsBodyType.STATIC, 0.0f, 0.0f, 0.0f, false), + target, + new ColliderComponent(new Vector3f(), new Quaternionf(), false), + shape, + material, + filter); + holder.addComponent(ChunkCollisionSourceComponent.getComponentType(), + new ChunkCollisionSourceComponent(mutation.sourceKey(), + mutation.chunkX(), + mutation.sectionY(), + mutation.chunkZ(), + payloadResourceKey, + partKind, + partIndex)); + Ref ref = store.addEntity(holder, AddReason.SPAWN); + assert ref != null; + store.getExternalData().putRefForUUID(bodyUuid, ref); + } + + @Nonnull + private static MaterialComponent material(@Nonnull Store store, + @Nonnull Ref spaceRef) { + MaterialComponent material = + store.getComponent(spaceRef, MaterialComponent.getComponentType()); + return material != null + ? material.clone() + : new MaterialComponent(PhysicsChunkCollisionDefaults.FRICTION, + PhysicsChunkCollisionDefaults.RESTITUTION); + } + + @Nonnull + private static CollisionFilterComponent filter(@Nonnull Store store, + @Nonnull Ref spaceRef) { + CollisionFilterComponent filter = + store.getComponent(spaceRef, CollisionFilterComponent.getComponentType()); + return filter != null + ? filter.clone() + : new CollisionFilterComponent(PhysicsChunkCollisionDefaults.COLLISION_GROUP, + PhysicsChunkCollisionDefaults.COLLISION_MASK); + } + + private static void removeGeneratedRows(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull Map> keys) { + if (keys.isEmpty()) { + return; + } + List rows = collectGeneratedRows(store, keys); + rows.sort((first, second) -> Integer.compare(second.ref().getIndex(), + first.ref().getIndex())); + PhysicsChunkCollisionPayloadResource chunkCollisionPayloads = store.getResource( + PhysicsChunkCollisionPayloadResource.getResourceType()); + List bodyEntityRemovals = + new ArrayList<>(rows.size()); + for (GeneratedRow row : rows) { + PhysicsStoreRowCleanup.removeRuntimeBody(store, runtime, row.uuid(), row.ref()); + removePayload(chunkCollisionPayloads, row.payloadResourceKey()); + bodyEntityRemovals.add(new PhysicsStoreRowCleanup.BodyEntityRemoval(row.uuid(), + row.ref())); + } + if (!bodyEntityRemovals.isEmpty()) { + PhysicsStoreRowCleanup.removeBodyEntities(store, bodyEntityRemovals); + PhysicsStoreRowCleanup.refreshIdentityAndRuntimeRefs(store); + } + } + + @Nonnull + private static List collectGeneratedRows(@Nonnull Store store, + @Nonnull Map> keys) { + ConcurrentLinkedQueue rows = new ConcurrentLinkedQueue<>(); + store.forEachEntityParallel(UuidComponent.getComponentType(), (index, chunk, _) -> { + UUID rowUuid = PhysicsStoreSystemSupport.rowUuid(chunk, index); + if (PhysicsStoreSystemSupport.isNil(rowUuid)) { + return; + } + ChunkCollisionSourceComponent source = chunk.getComponent(index, + ChunkCollisionSourceComponent.getComponentType()); + BodyComponent body = chunk.getComponent(index, BodyComponent.getComponentType()); + if (source != null + && body != null + && containsRemovalKey(keys, body, source)) { + rows.add(new GeneratedRow(chunk.getReferenceTo(index), + rowUuid, + source.getPayloadResourceKey())); + } + }); + return new ArrayList<>(rows); + } + + private static boolean containsRemovalKey( + @Nonnull Map> keys, + @Nonnull BodyComponent body, + @Nonnull ChunkCollisionSourceComponent source) { + ObjectOpenHashSet sourceKeys = keys.get(body.getSpaceUuid()); + return sourceKeys != null && sourceKeys.contains(source.getSourceKey()); + } + + private static void removePayload(@Nonnull PhysicsChunkCollisionPayloadResource chunkCollisionPayloads, + @Nullable String payloadResourceKey) { + if (payloadResourceKey != null && !payloadResourceKey.isBlank()) { + chunkCollisionPayloads.remove(payloadResourceKey); + } + } + + @Nonnull + public static UUID chunkCollisionBodyUuid(@Nonnull UUID spaceUuid, + @Nonnull String sourceKey, + @Nonnull PartKind partKind, + int partIndex) { + String key = spaceUuid + "|" + sourceKey + "|" + partKind.name() + "|" + partIndex; + return UUID.nameUUIDFromBytes(key.getBytes(StandardCharsets.UTF_8)); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + + private record GeneratedRow(@Nonnull Ref ref, + @Nonnull UUID uuid, + @Nullable String payloadResourceKey) { + private GeneratedRow { + Objects.requireNonNull(ref, "ref"); + Objects.requireNonNull(uuid, "uuid"); + } + } + + private record MutationKey(@Nonnull UUID spaceUuid, + @Nonnull String sourceKey) { + private MutationKey { + Objects.requireNonNull(spaceUuid, "spaceUuid"); + Objects.requireNonNull(sourceKey, "sourceKey"); + } + } + + private record PreparedUpsert(@Nonnull ChunkCollisionMutation mutation, + @Nonnull ChunkCollisionPayload payload, + @Nonnull Ref spaceRef, + @Nonnull MaterialComponent material, + @Nonnull CollisionFilterComponent filter, + boolean nativeVoxel) { + private PreparedUpsert { + Objects.requireNonNull(mutation, "mutation"); + Objects.requireNonNull(payload, "payload"); + Objects.requireNonNull(spaceRef, "spaceRef"); + Objects.requireNonNull(material, "material"); + Objects.requireNonNull(filter, "filter"); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionProducerSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionProducerSystem.java new file mode 100644 index 00000000..447aa3a1 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionProducerSystem.java @@ -0,0 +1,379 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.QuerySystem; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkMutationCache.TargetRefreshDecision; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkSectionAccessCache; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkCollisionStreamingResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkLifecycle; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkStreamingBounds; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.profiling.PhysicsChunkProfilingResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.profiling.PhysicsChunkProfilingResource.Snapshot; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.profiling.PhysicsChunkProfilingResource.StreamingTargetDiagnostic; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionMutationQueueResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource.BodyCursor; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkSettingsIndexResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkSettingsIndexResource.PhysicsChunkSpaceSettings; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.sync.PhysicsSyncSystem; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3d; + +/** + * Produces copied PhysicsStore chunk collision mutations from EntityStore and ChunkStore state. + */ +public final class ChunkCollisionProducerSystem extends TickingSystem + implements QuerySystem { + + @Nullable + private static volatile ComponentType playerType; + @Nullable + private static volatile ComponentType transformType; + @Nullable + private static volatile Query query; + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, PhysicsSyncSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + if (!PhysicsChunkLifecycle.isEnabled()) { + return; + } + + PhysicsChunkProfilingResource profiling = store.getResource( + PhysicsChunkProfilingResource.getResourceType()); + Snapshot snapshot = profiling.isEnabled() ? profiling.beginTick() : null; + long tickStart = snapshot != null ? System.nanoTime() : 0L; + try { + World world = store.getExternalData().getWorld(); + Store physics = PhysicsThreading.store(world); + PhysicsThreading.requireWorldThread(physics, + "produce PhysicsStore PhysicsChunk chunk collision mutations"); + PhysicsChunkCollisionMutationQueueResource queue = physics.getResource( + PhysicsChunkCollisionMutationQueueResource.getResourceType()); + PhysicsChunkSettingsIndexResource chunkCollisionSettingsIndex = physics.getResource( + PhysicsChunkSettingsIndexResource.getResourceType()); + queue.updateStamp(PhysicsChunkLifecycle.generation(), + chunkCollisionSettingsIndex.generation()); + PhysicsSnapshotResource snapshotResource = physics.getResource( + PhysicsSnapshotResource.getResourceType()); + PhysicsChunkCollisionStreamingResource streaming = store.getResource( + PhysicsChunkCollisionStreamingResource.getResourceType()); + + List spaces = + chunkCollisionSettingsIndex.streamingSpaces(); + if (spaces.isEmpty()) { + streaming.retainSpaces(Set.of(), queue); + return; + } + + List playerPositions = collectPlayerPositions(store, systemIndex); + if (snapshot != null) { + snapshot.setPlayerStreamingTargets(playerPositions.size()); + } + long currentTick = streaming.nextTick(); + Set retainedSpaces = new ObjectOpenHashSet<>(); + for (PhysicsChunkSpaceSettings settings : spaces) { + retainedSpaces.add(settings.spaceUuid()); + } + streaming.retainSpaces(retainedSpaces, queue); + + Map> bodyTargetsBySpace = + collectDynamicBodyTargetsBySpace(streaming, + spaces, + snapshotResource, + currentTick, + snapshot); + for (PhysicsChunkSpaceSettings settings : spaces) { + if (snapshot != null) { + snapshot.incrementStreamingSpaces(); + } + processSpace(world, + streaming, + queue, + settings, + playerPositions, + bodyTargetsBySpace.getOrDefault(settings.spaceUuid(), List.of()), + currentTick, + snapshot); + } + } finally { + if (snapshot != null) { + snapshot.setTickNanos(System.nanoTime() - tickStart); + profiling.finishTick(snapshot); + } + } + } + + private static void processSpace(@Nonnull World world, + @Nonnull PhysicsChunkCollisionStreamingResource streaming, + @Nonnull PhysicsChunkCollisionMutationQueueResource queue, + @Nonnull PhysicsChunkSpaceSettings settings, + @Nonnull List playerPositions, + @Nonnull List bodyTargets, + long currentTick, + @Nullable Snapshot snapshot) { + LongSet visitedSections = new LongOpenHashSet(); + PhysicsChunkSectionAccessCache accessCache = new PhysicsChunkSectionAccessCache(); + for (Vector3d position : playerPositions) { + int sectionsBefore = visitedSections.size(); + streaming.ensureAround(world, + settings.spaceUuid(), + queue, + position, + settings.radius(), + currentTick, + snapshot, + visitedSections, + snapshot != null ? StreamingTargetDiagnostic.player(position) : null, + accessCache, + settings.buildOptions()); + if (snapshot != null) { + snapshot.addPlayerSectionTargets(visitedSections.size() - sectionsBefore); + } + } + + for (BodyStreamingTarget target : bodyTargets) { + int sectionsBefore = visitedSections.size(); + streaming.ensureAround(world, + settings.spaceUuid(), + queue, + target.position(), + settings.bodyRadius(), + currentTick, + snapshot, + visitedSections, + null, + accessCache, + settings.buildOptions()); + for (BodyStreamingRefresh refresh : target.refreshes()) { + streaming.recordBodyTargetRefresh(settings.spaceUuid(), + refresh.bodyRef(), + target.bounds(), + refresh.sleeping(), + currentTick); + } + if (snapshot != null) { + snapshot.addBodySectionTargets(visitedSections.size() - sectionsBefore); + } + } + + streaming.pruneUnloaded(world, settings.spaceUuid(), queue, snapshot, accessCache); + streaming.pruneUnused(settings.spaceUuid(), queue, currentTick, settings.ttlTicks(), snapshot); + streaming.pruneBodyStreamingTargets(settings.spaceUuid(), + currentTick, + settings.ttlTicks(), + snapshot); + } + + @Nonnull + private static Map> collectDynamicBodyTargetsBySpace( + @Nonnull PhysicsChunkCollisionStreamingResource streaming, + @Nonnull List spaces, + @Nonnull PhysicsSnapshotResource snapshotResource, + long currentTick, + @Nullable Snapshot snapshot) { + Map settingsBySpace = new Object2ObjectOpenHashMap<>(); + Map accumulators = new Object2ObjectOpenHashMap<>(); + for (PhysicsChunkSpaceSettings settings : spaces) { + settingsBySpace.put(settings.spaceUuid(), settings); + accumulators.put(settings.spaceUuid(), new BodyTargetAccumulator()); + } + snapshotResource.forEachBodyCursor(body -> collectDynamicBodyTarget(streaming, + settingsBySpace, + accumulators, + body, + currentTick, + snapshot)); + + Map> targetsBySpace = new Object2ObjectOpenHashMap<>(); + if (snapshot != null) { + for (BodyTargetAccumulator accumulator : accumulators.values()) { + snapshot.addBodySpatialIndexCandidates(accumulator.spatialCandidates); + snapshot.addBodyStreamingCandidates(accumulator.dynamicCandidates); + snapshot.addBodyStreamingTargets(accumulator.targets.size()); + } + } + for (Map.Entry entry : accumulators.entrySet()) { + targetsBySpace.put(entry.getKey(), new ArrayList<>(entry.getValue().targets.values())); + } + return targetsBySpace; + } + + private static void collectDynamicBodyTarget( + @Nonnull PhysicsChunkCollisionStreamingResource streaming, + @Nonnull Map settingsBySpace, + @Nonnull Map accumulators, + @Nonnull BodyCursor body, + long currentTick, + @Nullable Snapshot snapshot) { + PhysicsChunkSpaceSettings settings = settingsBySpace.get(body.spaceUuid()); + if (settings == null) { + return; + } + BodyTargetAccumulator accumulator = accumulators.get(settings.spaceUuid()); + accumulator.spatialCandidates++; + if (body.bodyType() != PhysicsBodyType.DYNAMIC) { + return; + } + Ref bodyRef = body.bodyRef(); + if (bodyRef == null || !bodyRef.isValid()) { + return; + } + accumulator.dynamicCandidates++; + PhysicsChunkStreamingBounds bounds = PhysicsChunkStreamingBounds.from(body.positionX(), + body.positionY(), + body.positionZ(), + settings.bodyRadius()); + TargetRefreshDecision decision = streaming.shouldRefreshBodyTarget(settings.spaceUuid(), + bodyRef, + bounds, + body.sleeping(), + currentTick, + settings.ttlTicks(), + snapshot); + if (!decision.refresh()) { + return; + } + + BodyStreamingTarget target = accumulator.targets.get(bounds); + if (target == null) { + target = new BodyStreamingTarget(new Vector3d(body.positionX(), + body.positionY(), + body.positionZ()), + bounds, + new ArrayList<>()); + accumulator.targets.put(bounds, target); + } else if (snapshot != null) { + snapshot.incrementBodyTargetDedupeSkips(); + } + target.refreshes().add(new BodyStreamingRefresh(bodyRef, body.sleeping())); + } + + @Nonnull + private static List collectPlayerPositions(@Nonnull Store store, + int systemIndex) { + List playerPositions = new ArrayList<>(); + BiConsumer, CommandBuffer> collector = + (chunk, _) -> collectPlayerPositions(chunk, playerPositions); + store.forEachChunk(systemIndex, collector); + return List.copyOf(playerPositions); + } + + private static void collectPlayerPositions(@Nonnull ArchetypeChunk chunk, + @Nonnull List positions) { + for (int index = 0; index < chunk.size(); index++) { + TransformComponent transform = chunk.getComponent(index, transformType()); + if (transform != null) { + positions.add(new Vector3d(transform.getPosition())); + } + } + } + + @Nonnull + @Override + public Query getQuery() { + return query(); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + + @Nonnull + private static Query query() { + Query resolved = query; + if (resolved != null) { + return resolved; + } + synchronized (ChunkCollisionProducerSystem.class) { + resolved = query; + if (resolved == null) { + resolved = Query.and(playerType(), transformType()); + query = resolved; + } + } + return resolved; + } + + @Nonnull + private static ComponentType playerType() { + ComponentType resolved = playerType; + if (resolved != null) { + return resolved; + } + synchronized (ChunkCollisionProducerSystem.class) { + resolved = playerType; + if (resolved == null) { + resolved = Player.getComponentType(); + playerType = resolved; + } + } + return resolved; + } + + @Nonnull + private static ComponentType transformType() { + ComponentType resolved = transformType; + if (resolved != null) { + return resolved; + } + synchronized (ChunkCollisionProducerSystem.class) { + resolved = transformType; + if (resolved == null) { + resolved = TransformComponent.getComponentType(); + transformType = resolved; + } + } + return resolved; + } + + private record BodyStreamingTarget(@Nonnull Vector3d position, + @Nonnull PhysicsChunkStreamingBounds bounds, + @Nonnull List refreshes) { + } + + private record BodyStreamingRefresh(@Nonnull Ref bodyRef, + boolean sleeping) { + } + + private static final class BodyTargetAccumulator { + + private final Map targets = + new Object2ObjectOpenHashMap<>(); + private int spatialCandidates; + private int dynamicCandidates; + } + +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionRestorePrewarmSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionRestorePrewarmSystem.java new file mode 100644 index 00000000..b530a0f3 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionRestorePrewarmSystem.java @@ -0,0 +1,39 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkStoreTypes; +import dev.hytalemodding.impulse.core.internal.systems.SpaceSettingsApplicationSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.BodyBindingSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.SpaceBindingSystem; +import java.util.Set; +import javax.annotation.Nonnull; + +/** + * Requests PhysicsChunk collision support for unbound restored bodies before body binding. + */ +public final class ChunkCollisionRestorePrewarmSystem extends TickingSystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, SpaceBindingSystem.class), + new SystemDependency<>(Order.AFTER, SpaceSettingsApplicationSystem.class), + new SystemDependency<>(Order.AFTER, PhysicsChunkSettingsIndexSystem.class), + new SystemDependency<>(Order.BEFORE, ChunkCollisionMutationDrainSystem.class), + new SystemDependency<>(Order.BEFORE, BodyBindingSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + PhysicsChunkStoreTypes.prewarmRestoreDependencies(store); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionVoxelStitchingSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionVoxelStitchingSystem.java new file mode 100644 index 00000000..c37fe706 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionVoxelStitchingSystem.java @@ -0,0 +1,197 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.QuerySystem; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionPayloadResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionPayload; +import dev.hytalemodding.impulse.core.internal.systems.PhysicsStoreSystemSupport; +import dev.hytalemodding.impulse.core.internal.systems.binding.BodyBindingSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.TargetBindingSystem; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent.PartKind; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Applies native voxel terrain adjacency hints for generated PhysicsChunk body rows. + */ +public final class ChunkCollisionVoxelStitchingSystem extends TickingSystem + implements QuerySystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, BodyBindingSystem.class), + new SystemDependency<>(Order.BEFORE, TargetBindingSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + if (restore.isFailed()) { + return; + } + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + PhysicsChunkCollisionPayloadResource payloads = store.getResource( + PhysicsChunkCollisionPayloadResource.getResourceType()); + Set stitchedPairs = new ObjectOpenHashSet<>(); + BiConsumer, CommandBuffer> collector = + (chunk, _) -> stitchChunk(store, runtime, payloads, restore, stitchedPairs, chunk); + store.forEachChunk(systemIndex, collector); + } + + private static void stitchChunk(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsChunkCollisionPayloadResource payloads, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull Set stitchedPairs, + @Nonnull ArchetypeChunk chunk) { + for (int index = 0; index < chunk.size(); index++) { + ChunkCollisionSourceComponent source = chunk.getComponent(index, + ChunkCollisionSourceComponent.getComponentType()); + if (source == null || source.getPartKind() != PartKind.NATIVE_VOXELS) { + continue; + } + Ref bodyRef = chunk.getReferenceTo(index); + if (runtime.isChunkCollisionPayloadBound(bodyRef, source.getPayloadResourceKey())) { + continue; + } + BodyComponent body = chunk.getComponent(index, BodyComponent.getComponentType()); + if (body == null) { + continue; + } + stitchBody(runtime, + store, + payloads, + restore, + stitchedPairs, + bodyRef, + body, + source); + } + } + + private static void stitchBody(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull Store store, + @Nonnull PhysicsChunkCollisionPayloadResource payloads, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull Set stitchedPairs, + @Nonnull Ref bodyRef, + @Nonnull BodyComponent body, + @Nonnull ChunkCollisionSourceComponent source) { + BackendBodyHandle bodyHandle = runtime.getBodyHandle(bodyRef); + BackendSpaceHandle spaceHandle = runtime.getBodySpaceHandle(bodyRef); + BackendId backendId = runtime.getBodyBackendId(bodyRef); + if (bodyHandle == null || spaceHandle == null || backendId == null) { + return; + } + PhysicsBackendRuntime backendRuntime = runtime.runtimeForBodyRef(bodyRef); + if (backendRuntime == null) { + restore.recordSoftSkip("Voxel terrain backend runtime is missing: " + + source.getSourceKey()); + return; + } + ChunkCollisionPayload payload = payloads.get(source.getPayloadResourceKey()); + if (payload == null) { + restore.recordSoftSkip("Voxel chunk collision payload is missing: " + source.getSourceKey()); + return; + } + for (ChunkCollisionPayload.Neighbor neighbor : payload.neighbors()) { + stitchNeighbor(runtime, + store, + backendRuntime, + backendId, + spaceHandle, + body.getSpaceUuid(), + bodyHandle, + neighbor, + stitchedPairs); + } + runtime.markChunkCollisionPayloadBound(bodyRef, source.getPayloadResourceKey()); + } + + private static void stitchNeighbor(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull Store store, + @Nonnull PhysicsBackendRuntime backendRuntime, + @Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull UUID spaceUuid, + @Nonnull BackendBodyHandle bodyHandle, + @Nonnull ChunkCollisionPayload.Neighbor neighbor, + @Nonnull Set stitchedPairs) { + Ref neighborRef = neighborRef(store, spaceUuid, neighbor.sourceKey()); + if (neighborRef == null) { + return; + } + BackendBodyHandle neighborBody = runtime.getBodyHandle(neighborRef); + BackendSpaceHandle neighborSpace = runtime.getBodySpaceHandle(neighborRef); + BackendId neighborBackendId = runtime.getBodyBackendId(neighborRef); + if (neighborBody == null + || neighborSpace == null + || neighborBackendId == null + || !neighborBackendId.equals(backendId) + || neighborSpace.value() != spaceHandle.value()) { + return; + } + BodyPair pair = BodyPair.of(bodyHandle.value(), neighborBody.value()); + if (!stitchedPairs.add(pair)) { + return; + } + backendRuntime.combineVoxelTerrains(spaceHandle.value(), + bodyHandle.value(), + neighborBody.value(), + neighbor.shiftX(), + neighbor.shiftY(), + neighbor.shiftZ()); + } + + @Nullable + private static Ref neighborRef(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull String sourceKey) { + UUID neighborUuid = ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + sourceKey, + PartKind.NATIVE_VOXELS, + 0); + Ref ref = store.getExternalData().getRefFromUUID(neighborUuid); + return ref != null && ref.getStore() == store && ref.isValid() ? ref : null; + } + + @Nonnull + @Override + public Query getQuery() { + return PhysicsStoreSystemSupport.uuidQuery(); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + + private record BodyPair(long first, long second) { + + private static BodyPair of(long first, long second) { + return first <= second ? new BodyPair(first, second) : new BodyPair(second, first); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/PhysicsChunkSettingsIndexSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/PhysicsChunkSettingsIndexSystem.java new file mode 100644 index 00000000..63aa49dc --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/PhysicsChunkSettingsIndexSystem.java @@ -0,0 +1,92 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.QuerySystem; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkLifecycle; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionMutationQueueResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkSettingsIndexResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkSettingsIndexResource.PhysicsChunkSpaceSettings; +import dev.hytalemodding.impulse.core.internal.systems.IdentityIndexSystem; +import dev.hytalemodding.impulse.core.internal.systems.PhysicsStoreSystemSupport; +import dev.hytalemodding.impulse.core.internal.systems.binding.SpaceBindingSystem; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.ChunkCollisionSettingsComponent; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; + +/** + * Publishes copied PhysicsChunk collision settings for PhysicsStore space entities. + */ +public final class PhysicsChunkSettingsIndexSystem extends TickingSystem + implements QuerySystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, IdentityIndexSystem.class), + new SystemDependency<>(Order.BEFORE, SpaceBindingSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + Map settingsBySpaceUuid = + new Object2ObjectOpenHashMap<>(); + BiConsumer, CommandBuffer> collector = + (chunk, _) -> collectChunk(settingsBySpaceUuid, chunk); + store.forEachChunk(systemIndex, collector); + PhysicsChunkSettingsIndexResource settingsIndex = + store.getResource(PhysicsChunkSettingsIndexResource.getResourceType()); + settingsIndex.replaceAll(settingsBySpaceUuid); + store.getResource(PhysicsChunkCollisionMutationQueueResource.getResourceType()) + .updateStamp(PhysicsChunkLifecycle.generation(), settingsIndex.generation()); + } + + private static void collectChunk( + @Nonnull Map settingsBySpaceUuid, + @Nonnull ArchetypeChunk chunk) { + for (int index = 0; index < chunk.size(); index++) { + SpaceComponent space = chunk.getComponent(index, SpaceComponent.getComponentType()); + if (space == null) { + continue; + } + UUID spaceUuid = PhysicsStoreSystemSupport.rowUuid(chunk, index); + if (PhysicsStoreSystemSupport.isNil(spaceUuid)) { + continue; + } + ChunkCollisionSettingsComponent chunkCollision = chunk.getComponent(index, + ChunkCollisionSettingsComponent.getComponentType()); + ChunkCollisionSettingsComponent settings = chunkCollision != null + ? chunkCollision + : new ChunkCollisionSettingsComponent(); + settingsBySpaceUuid.put(spaceUuid, new PhysicsChunkSpaceSettings(spaceUuid, + settings.getMode(), + settings.getEntityChunkBoundaryMode(), + settings.isNativeVoxelCollisionEnabled(), + settings.getRadius(), + settings.getBodyRadius(), + settings.getTtlTicks())); + } + } + + @Nonnull + @Override + public Query getQuery() { + return PhysicsStoreSystemSupport.uuidQuery(); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/diagnostics/PhysicsEntityDiagnostics.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityDiagnostics.java similarity index 79% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/diagnostics/PhysicsEntityDiagnostics.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityDiagnostics.java index 277a2f9f..2f6d3f91 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/diagnostics/PhysicsEntityDiagnostics.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityDiagnostics.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.diagnostics; +package dev.hytalemodding.impulse.core.internal.modules.physicsentity; import com.hypixel.hytale.component.ComponentType; import com.hypixel.hytale.component.Store; @@ -7,8 +7,8 @@ import com.hypixel.hytale.server.core.modules.entity.tracker.EntityTrackerSystems.Visible; import com.hypixel.hytale.server.core.modules.entity.tracker.NetworkId; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent.AttachmentLifecycle; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent.AttachmentLifecycle; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicIntegerArray; import javax.annotation.Nonnull; @@ -18,16 +18,6 @@ */ public final class PhysicsEntityDiagnostics { - private static final ComponentType ATTACHMENT_TYPE = - PhysicsBodyAttachmentComponent.getComponentType(); - private static final ComponentType TRANSFORM_TYPE = - TransformComponent.getComponentType(); - private static final ComponentType NETWORK_ID_TYPE = - NetworkId.getComponentType(); - private static final ComponentType VISIBLE_TYPE = - Visible.getComponentType(); - private static final ComponentType ENTITY_VIEWER_TYPE = - EntityViewer.getComponentType(); private static final int BODY_PHYSICS_BODIES = 0; private static final int BODY_WITH_TRANSFORM = 1; private static final int BODY_WITH_NETWORK_ID = 2; @@ -43,23 +33,33 @@ private PhysicsEntityDiagnostics() { @Nonnull public static Snapshot collect(@Nonnull Store store) { + ComponentType attachmentType = + BodyAttachmentComponent.getComponentType(); + ComponentType transformType = + TransformComponent.getComponentType(); + ComponentType networkIdType = + NetworkId.getComponentType(); + ComponentType visibleType = + Visible.getComponentType(); + ComponentType entityViewerType = + EntityViewer.getComponentType(); EntityFootprint bodyFootprint = collectBodyFootprint(store, - ATTACHMENT_TYPE, - TRANSFORM_TYPE, - NETWORK_ID_TYPE, - VISIBLE_TYPE); + attachmentType, + transformType, + networkIdType, + visibleType); VisualFootprint visualFootprint = collectVisualFootprint(store, - ATTACHMENT_TYPE, - TRANSFORM_TYPE, - NETWORK_ID_TYPE); + attachmentType, + transformType, + networkIdType); return new Snapshot(bodyFootprint.physicsBodies(), 0, visualFootprint.visuals(), - count(store, TRANSFORM_TYPE), - count(store, NETWORK_ID_TYPE), - count(store, VISIBLE_TYPE), - count(store, ENTITY_VIEWER_TYPE), + count(store, transformType), + count(store, networkIdType), + count(store, visibleType), + count(store, entityViewerType), bodyFootprint.withTransform(), bodyFootprint.withNetworkId(), bodyFootprint.withVisible(), @@ -68,7 +68,7 @@ public static Snapshot collect(@Nonnull Store store) { } private static EntityFootprint collectBodyFootprint(@Nonnull Store store, - @Nonnull ComponentType attachmentType, + @Nonnull ComponentType attachmentType, @Nonnull ComponentType transformType, @Nonnull ComponentType networkIdType, @Nonnull ComponentType visibleType) { @@ -98,12 +98,12 @@ private static EntityFootprint collectBodyFootprint(@Nonnull Store } private static VisualFootprint collectVisualFootprint(@Nonnull Store store, - @Nonnull ComponentType attachmentType, + @Nonnull ComponentType attachmentType, @Nonnull ComponentType transformType, @Nonnull ComponentType networkIdType) { AtomicIntegerArray counters = new AtomicIntegerArray(VISUAL_COUNTERS); store.forEachEntityParallel(attachmentType, (index, chunk, _) -> { - PhysicsBodyAttachmentComponent attachment = chunk.getComponent(index, attachmentType); + BodyAttachmentComponent attachment = chunk.getComponent(index, attachmentType); if (attachment == null || attachment.getLifecycle() != AttachmentLifecycle.GENERATED_PROXY) { return; } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityLifecycle.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityLifecycle.java new file mode 100644 index 00000000..dfd23008 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityLifecycle.java @@ -0,0 +1,32 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicsentity; + +import dev.hytalemodding.impulse.core.internal.modules.SubPluginLifecycleGate; + +/** + * Server-level lifecycle controlled by the Impulse PhysicsEntity subplugin. + */ +public final class PhysicsEntityLifecycle { + + public static final String DISABLED_MESSAGE = + "Impulse PhysicsEntity integration is not available. " + + "Enable HytaleModding:ImpulsePhysicsEntity to use EntityStore physics projections."; + + private static final SubPluginLifecycleGate GATE = + new SubPluginLifecycleGate(DISABLED_MESSAGE); + + private PhysicsEntityLifecycle() { + } + + public static void enable() { + GATE.enable(); + } + + public static void disable() { + GATE.disable(); + } + + public static boolean isEnabled() { + return GATE.isEnabled(); + } + +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityModule.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityModule.java new file mode 100644 index 00000000..b2d1a9d4 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityModule.java @@ -0,0 +1,37 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicsentity; + +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.server.core.plugin.JavaPlugin; +import com.hypixel.hytale.server.core.plugin.JavaPluginInit; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.commands.PhysicsEntityCommandSet; +import javax.annotation.Nonnull; + +/** + * Bundled subplugin that integrates authoritative PhysicsStore bodies with EntityStore entities. + */ +public final class PhysicsEntityModule extends JavaPlugin { + + public PhysicsEntityModule(@Nonnull JavaPluginInit init) { + super(init); + } + + @Override + protected void setup() { + ComponentRegistryProxy entityRegistry = getEntityStoreRegistry(); + PhysicsEntityTypeRegistry.registerComponentTypes(entityRegistry); + PhysicsEntityTypeRegistry.registerResourceTypes(entityRegistry); + PhysicsEntityTypeRegistry.registerEventTypes(entityRegistry); + PhysicsEntityTypeRegistry.registerSystemGroups(entityRegistry); + PhysicsEntityTypeRegistry.registerSystems(entityRegistry); + PhysicsEntityCommandSet.register(); + PhysicsEntityLifecycle.enable(); + } + + @Override + protected void shutdown() { + PhysicsEntityLifecycle.disable(); + PhysicsEntityCommandSet.unregister(); + PhysicsEntityTypeRegistry.clearEntityStoreTypes(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityProjectionCleanup.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityProjectionCleanup.java new file mode 100644 index 00000000..3cf3fe53 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityProjectionCleanup.java @@ -0,0 +1,215 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicsentity; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.RemoveReason; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsEntityAttachments; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.GeneratedVisualProxyComponent; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicIntegerArray; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3d; + +/** + * Module-owned cleanup policy for EntityStore physics projections. + */ +public final class PhysicsEntityProjectionCleanup { + + private static final int REMOVED_ATTACHMENT_ENTITIES = 0; + private static final int DETACHED_EXTERNAL_ATTACHMENTS = 1; + private static final int REMOVED_ORPHAN_VISUAL_ENTITIES = 2; + private static final int COUNTERS = 3; + + private PhysicsEntityProjectionCleanup() { + } + + @Nonnull + public static Result cleanAll(@Nonnull Store store, + @Nullable ComponentType> detachableMarkerType) { + return cleanAll(store, markerList(detachableMarkerType)); + } + + @Nonnull + public static Result cleanAll(@Nonnull Store store, + @Nonnull Collection>> + detachableMarkerTypes) { + if (!PhysicsEntityAttachments.isAvailable()) { + return Result.skippedResult(); + } + Collection>> checkedMarkerTypes = + Objects.requireNonNull(detachableMarkerTypes, "detachableMarkerTypes"); + AtomicIntegerArray counters = new AtomicIntegerArray(COUNTERS); + ComponentType attachmentType = + BodyAttachmentComponent.getComponentType(); + ComponentType generatedProxyType = + GeneratedVisualProxyComponent.getComponentType(); + + store.forEachEntityParallel(attachmentType, + (index, archetypeChunk, commandBuffer) -> { + BodyAttachmentComponent attachment = archetypeChunk.getComponent(index, + attachmentType); + if (attachment == null) { + return; + } + cleanAttachedEntity(counters, + commandBuffer, + archetypeChunk.getReferenceTo(index), + attachmentType, + checkedMarkerTypes, + attachment); + }); + + store.forEachEntityParallel(generatedProxyType, + (index, archetypeChunk, commandBuffer) -> { + if (archetypeChunk.getComponent(index, attachmentType) != null) { + return; + } + removeOrphanProxy(counters, commandBuffer, archetypeChunk.getReferenceTo(index)); + }); + + return Result.from(counters); + } + + @Nonnull + public static Result cleanSelected(@Nonnull Store store, + @Nonnull Set selectedBodyUuids, + @Nonnull Vector3d center, + double radiusSquared, + @Nullable ComponentType> detachableMarkerType) { + return cleanSelected(store, + selectedBodyUuids, + center, + radiusSquared, + markerList(detachableMarkerType)); + } + + @Nonnull + public static Result cleanSelected(@Nonnull Store store, + @Nonnull Set selectedBodyUuids, + @Nonnull Vector3d center, + double radiusSquared, + @Nonnull Collection>> + detachableMarkerTypes) { + if (!PhysicsEntityAttachments.isAvailable()) { + return Result.skippedResult(); + } + Set checkedBodyUuids = Objects.requireNonNull(selectedBodyUuids, + "selectedBodyUuids"); + Vector3d checkedCenter = Objects.requireNonNull(center, "center"); + Collection>> checkedMarkerTypes = + Objects.requireNonNull(detachableMarkerTypes, "detachableMarkerTypes"); + AtomicIntegerArray counters = new AtomicIntegerArray(COUNTERS); + ComponentType attachmentType = + BodyAttachmentComponent.getComponentType(); + ComponentType generatedProxyType = + GeneratedVisualProxyComponent.getComponentType(); + + store.forEachEntityParallel(attachmentType, + (index, archetypeChunk, commandBuffer) -> { + BodyAttachmentComponent attachment = archetypeChunk.getComponent(index, + attachmentType); + if (attachment == null || !checkedBodyUuids.contains(attachment.getBodyUuid())) { + return; + } + cleanAttachedEntity(counters, + commandBuffer, + archetypeChunk.getReferenceTo(index), + attachmentType, + checkedMarkerTypes, + attachment); + }); + + store.forEachEntityParallel(generatedProxyType, + (index, archetypeChunk, commandBuffer) -> { + if (archetypeChunk.getComponent(index, attachmentType) != null + || !entityWithinRadius(archetypeChunk, index, checkedCenter, radiusSquared)) { + return; + } + removeOrphanProxy(counters, commandBuffer, archetypeChunk.getReferenceTo(index)); + }); + + return Result.from(counters); + } + + private static void cleanAttachedEntity( + @Nonnull AtomicIntegerArray counters, + @Nonnull CommandBuffer commandBuffer, + @Nonnull Ref entityRef, + @Nonnull ComponentType attachmentType, + @Nonnull Collection>> + detachableMarkerTypes, + @Nonnull BodyAttachmentComponent attachment) { + if (attachment.shouldRemoveEntityWhenBodyMissing()) { + counters.incrementAndGet(REMOVED_ATTACHMENT_ENTITIES); + commandBuffer.removeEntity(entityRef, RemoveReason.REMOVE); + return; + } + counters.incrementAndGet(DETACHED_EXTERNAL_ATTACHMENTS); + removeMarkers(commandBuffer, entityRef, detachableMarkerTypes); + commandBuffer.removeComponent(entityRef, attachmentType); + } + + private static void removeMarkers(@Nonnull CommandBuffer commandBuffer, + @Nonnull Ref entityRef, + @Nonnull Collection>> + markerTypes) { + for (ComponentType> markerType : markerTypes) { + if (commandBuffer.getComponent(entityRef, markerType) != null) { + commandBuffer.removeComponent(entityRef, markerType); + } + } + } + + @Nonnull + private static List>> markerList( + @Nullable ComponentType> markerType) { + return markerType != null ? List.of(markerType) : List.of(); + } + + private static void removeOrphanProxy(@Nonnull AtomicIntegerArray counters, + @Nonnull CommandBuffer commandBuffer, + @Nonnull Ref entityRef) { + counters.incrementAndGet(REMOVED_ORPHAN_VISUAL_ENTITIES); + commandBuffer.removeEntity(entityRef, RemoveReason.REMOVE); + } + + private static boolean entityWithinRadius(@Nonnull ArchetypeChunk archetypeChunk, + int index, + @Nonnull Vector3d center, + double radiusSquared) { + TransformComponent transform = + archetypeChunk.getComponent(index, TransformComponent.getComponentType()); + return transform != null && transform.getPosition().distanceSquared(center) <= radiusSquared; + } + + public record Result(boolean skipped, + int removedAttachmentEntities, + int detachedExternalAttachments, + int removedOrphanVisualEntities) { + + @Nonnull + private static Result skippedResult() { + return new Result(true, 0, 0, 0); + } + + @Nonnull + private static Result from(@Nonnull AtomicIntegerArray counters) { + return new Result(false, + counters.get(REMOVED_ATTACHMENT_ENTITIES), + counters.get(DETACHED_EXTERNAL_ATTACHMENTS), + counters.get(REMOVED_ORPHAN_VISUAL_ENTITIES)); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityTypeRegistry.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityTypeRegistry.java new file mode 100644 index 00000000..d16fd5e6 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityTypeRegistry.java @@ -0,0 +1,155 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicsentity; + +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.SystemGroup; +import com.hypixel.hytale.component.event.WorldEventType; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources.PhysicsBodySyncStateResource; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources.PhysicsDebugOverlayResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsProjectionIndexResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualInterestResource; +import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; +import dev.hytalemodding.impulse.core.internal.systems.debug.PhysicsDebugSystem; +import dev.hytalemodding.impulse.core.internal.systems.publication.PhysicsStoreEventPublicationSystem; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.sync.PhysicsBodyAttachmentIndexSystem; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.sync.PhysicsSyncSystem; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.visual.PhysicsProjectionCleanupSystem; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFramePublishedEvent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.GeneratedVisualProxyComponent; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Registered EntityStore type handles owned by the PhysicsEntity subplugin. + */ +public final class PhysicsEntityTypeRegistry { + + @Nullable + private static ComponentType bodyAttachmentComponentType; + @Nullable + private static ComponentType + generatedVisualProxyComponentType; + @Nullable + private static WorldEventType + physicsEventFramePublishedEventType; + @Nullable + private static SystemGroup persistenceRestoreGroup; + + private PhysicsEntityTypeRegistry() { + } + + public static void registerComponentTypes(@Nonnull ComponentRegistryProxy registry) { + bodyAttachmentComponentType = registry.registerComponent( + BodyAttachmentComponent.class, + "BodyAttachment", + BodyAttachmentComponent.CODEC); + generatedVisualProxyComponentType = registry.registerComponent( + GeneratedVisualProxyComponent.class, + "GeneratedVisualProxy", + GeneratedVisualProxyComponent.CODEC); + } + + public static void registerResourceTypes(@Nonnull ComponentRegistryProxy registry) { + PhysicsDebugOverlayResource.setResourceType(registry.registerResource( + PhysicsDebugOverlayResource.class, + PhysicsDebugOverlayResource::new)); + PhysicsRuntimeProfilingResource.setResourceType(registry.registerResource( + PhysicsRuntimeProfilingResource.class, + PhysicsRuntimeProfilingResource::new)); + PhysicsProjectionIndexResource.setResourceType(registry.registerResource( + PhysicsProjectionIndexResource.class, + PhysicsProjectionIndexResource::new)); + PhysicsBodySyncStateResource.setResourceType(registry.registerResource( + PhysicsBodySyncStateResource.class, + PhysicsBodySyncStateResource::new)); + PhysicsVisualInterestResource.setResourceType(registry.registerResource( + PhysicsVisualInterestResource.class, + PhysicsVisualInterestResource::new)); + } + + public static void registerEventTypes(@Nonnull ComponentRegistryProxy registry) { + physicsEventFramePublishedEventType = + registry.registerWorldEventType(PhysicsEventFramePublishedEvent.class); + } + + public static void registerSystemGroups(@Nonnull ComponentRegistryProxy registry) { + persistenceRestoreGroup = registry.registerSystemGroup(); + } + + public static void registerSystems(@Nonnull ComponentRegistryProxy registry) { + registry.registerSystem(new PhysicsBodyAttachmentIndexSystem()); + registry.registerSystem(new PhysicsProjectionCleanupSystem()); + registry.registerSystem(new PhysicsSyncSystem()); + registry.registerSystem(new PhysicsDebugSystem()); + registry.registerSystem(new PhysicsStoreEventPublicationSystem()); + } + + public static void clearEntityStoreTypes() { + bodyAttachmentComponentType = null; + generatedVisualProxyComponentType = null; + physicsEventFramePublishedEventType = null; + persistenceRestoreGroup = null; + PhysicsDebugOverlayResource.clearResourceType(); + PhysicsRuntimeProfilingResource.clearResourceType(); + PhysicsProjectionIndexResource.clearResourceType(); + PhysicsBodySyncStateResource.clearResourceType(); + PhysicsVisualInterestResource.clearResourceType(); + } + + public static boolean areEntityStoreTypesRegistered() { + return bodyAttachmentComponentType != null + && generatedVisualProxyComponentType != null + && physicsEventFramePublishedEventType != null + && persistenceRestoreGroup != null + && PhysicsDebugOverlayResource.getResourceType() != null + && PhysicsRuntimeProfilingResource.getResourceType() != null + && PhysicsProjectionIndexResource.getResourceType() != null + && PhysicsBodySyncStateResource.getResourceType() != null + && PhysicsVisualInterestResource.getResourceType() != null; + } + + public static boolean isBodyAttachmentComponentTypeRegistered() { + return bodyAttachmentComponentType != null; + } + + public static boolean isGeneratedVisualProxyComponentTypeRegistered() { + return generatedVisualProxyComponentType != null; + } + + @Nonnull + public static ComponentType bodyAttachmentComponentType() { + return requireRegistered(bodyAttachmentComponentType, + "Impulse BodyAttachment component type is not registered"); + } + + @Nonnull + public static ComponentType + generatedVisualProxyComponentType() { + return requireRegistered(generatedVisualProxyComponentType, + "Impulse GeneratedVisualProxy component type is not registered"); + } + + @Nonnull + public static WorldEventType + physicsEventFramePublishedEventType() { + return requireRegistered(physicsEventFramePublishedEventType, + "Impulse physics event-frame world event type is not registered"); + } + + @Nonnull + public static SystemGroup persistenceRestoreGroup() { + return requireRegistered(persistenceRestoreGroup, + "Impulse PhysicsEntity persistence restore group is not registered"); + } + + @Nonnull + private static T requireRegistered(@Nullable T value, @Nonnull String message) { + if (value == null) { + throw new IllegalStateException(Objects.requireNonNull(message, "message")); + } + return value; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/commands/PhysicsEntityCommandSet.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/commands/PhysicsEntityCommandSet.java new file mode 100644 index 00000000..2cbaa34c --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/commands/PhysicsEntityCommandSet.java @@ -0,0 +1,24 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.commands; + +import dev.hytalemodding.impulse.core.internal.commands.ImpulseCommandTreeRegistry; + +/** + * Command set owned by the PhysicsEntity subplugin. + */ +public final class PhysicsEntityCommandSet { + + private static final String VISUAL_SETTINGS_COMMAND_ID = "physicsentity.settings.visual"; + + private PhysicsEntityCommandSet() { + } + + public static void register() { + ImpulseCommandTreeRegistry.registerSettingsSubCommand( + VISUAL_SETTINGS_COMMAND_ID, + VisualSettingsCommand::new); + } + + public static void unregister() { + ImpulseCommandTreeRegistry.unregisterSettingsSubCommand(VISUAL_SETTINGS_COMMAND_ID); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/VisualMaterializationSettingsCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/commands/VisualMaterializationSettingsCommand.java similarity index 68% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/VisualMaterializationSettingsCommand.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/commands/VisualMaterializationSettingsCommand.java index 277a355d..7e205dad 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/VisualMaterializationSettingsCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/commands/VisualMaterializationSettingsCommand.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.commands.settings; +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.commands; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; @@ -10,15 +10,18 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.commands.SpaceSelection; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsVisualMaterializationSettings; +import dev.hytalemodding.impulse.core.internal.commands.space.SpaceSelection; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsSpaces; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualMaterializationSettings; import java.util.Locale; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; +// NOTE: there's already a system in hytale that handles Entities Materialization/Visibility, we +// should probably integrate with that public class VisualMaterializationSettingsCommand extends AbstractAsyncPlayerCommand { private final OptionalArg enabledArg = this.withOptionalArg( @@ -87,14 +90,23 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); - SpaceId spaceId = SpaceSelection.resolve(ctx, world, resource, spaceArg); - if (spaceId == null) { + Store physicsStore = PhysicsThreading.store(world); + SpaceSelection.SelectedSpace selectedSpace = SpaceSelection.resolveStoreSpace(ctx, + world, + spaceArg); + if (selectedSpace == null) { return CompletableFuture.completedFuture(null); } + SpaceId spaceId = selectedSpace.spaceId(); - PhysicsSpaceSettings settings = new PhysicsSpaceSettings( - resource.getSpaceSettings(spaceId)); + PhysicsVisualMaterializationSettings settings = + PhysicsSpaces.visualMaterializationSettings(physicsStore, + selectedSpace.spaceRef()); + if (settings == null) { + ctx.sender().sendMessage(Message.raw("Physics space id=" + spaceId.value() + + " no longer exists.")); + return CompletableFuture.completedFuture(null); + } if (!anyArgProvided(ctx)) { sendSummary(ctx, spaceId, settings); return CompletableFuture.completedFuture(null); @@ -106,44 +118,46 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, ctx.sender().sendMessage(Message.raw("enabled must be true or false.")); return CompletableFuture.completedFuture(null); } - settings.getVisualMaterializationSettings().setDetachedVisualMaterializationEnabled(enabled); + settings.setDetachedVisualMaterializationEnabled(enabled); } int materializationRadius = materializationRadiusArg.provided(ctx) ? materializationRadiusArg.get(ctx) - : settings.getVisualMaterializationSettings().getDetachedVisualMaterializationRadius(); + : settings.getDetachedVisualMaterializationRadius(); int dematerializationRadius = dematerializationRadiusArg.provided(ctx) ? dematerializationRadiusArg.get(ctx) - : settings.getVisualMaterializationSettings().getDetachedVisualDematerializationRadius(); + : settings.getDetachedVisualDematerializationRadius(); try { - settings.getVisualMaterializationSettings().setDetachedVisualRadii(materializationRadius, dematerializationRadius); + settings.setDetachedVisualRadii(materializationRadius, dematerializationRadius); if (interestIntervalArg.provided(ctx)) { - settings.getVisualMaterializationSettings().setDetachedVisualInterestRefreshIntervalTicks( + settings.setDetachedVisualInterestRefreshIntervalTicks( interestIntervalArg.get(ctx)); } if (candidateIntervalArg.provided(ctx)) { - settings.getVisualMaterializationSettings().setDetachedVisualCandidateRefreshIntervalTicks( + settings.setDetachedVisualCandidateRefreshIntervalTicks( candidateIntervalArg.get(ctx)); } if (visibilityIntervalArg.provided(ctx)) { - settings.getVisualMaterializationSettings().setDetachedVisualVisibilityCheckIntervalTicks( + settings.setDetachedVisualVisibilityCheckIntervalTicks( visibilityIntervalArg.get(ctx)); } if (spawnRateArg.provided(ctx)) { - settings.getVisualMaterializationSettings().setDetachedVisualMaxSpawnsPerTick(spawnRateArg.get(ctx)); + settings.setDetachedVisualMaxSpawnsPerTick(spawnRateArg.get(ctx)); } if (capArg.provided(ctx)) { - settings.getVisualMaterializationSettings().setDetachedVisualMaxMaterialized(capArg.get(ctx)); + settings.setDetachedVisualMaxMaterialized(capArg.get(ctx)); } if (blockTypeArg.provided(ctx)) { - settings.getVisualMaterializationSettings().setDetachedVisualBlockType(blockTypeArg.get(ctx)); + settings.setDetachedVisualBlockType(blockTypeArg.get(ctx)); } } catch (IllegalArgumentException exception) { ctx.sender().sendMessage(Message.raw(exception.getMessage())); return CompletableFuture.completedFuture(null); } - resource.setSpaceSettings(spaceId, settings); + PhysicsSpaces.putVisualMaterializationSettings(physicsStore, + selectedSpace.spaceRef(), + settings); sendSummary(ctx, spaceId, settings); return CompletableFuture.completedFuture(null); } @@ -162,18 +176,18 @@ private boolean anyArgProvided(@Nonnull CommandContext ctx) { private static void sendSummary(@Nonnull CommandContext ctx, @Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceSettings settings) { + @Nonnull PhysicsVisualMaterializationSettings settings) { ctx.sender().sendMessage(Message.raw("Impulse visual materialization settings for space " + spaceId.value() - + ": enabled=" + settings.getVisualMaterializationSettings().isDetachedVisualMaterializationEnabled() - + " materializationRadius=" + settings.getVisualMaterializationSettings().getDetachedVisualMaterializationRadius() - + " dematerializationRadius=" + settings.getVisualMaterializationSettings().getDetachedVisualDematerializationRadius() - + " interestInterval=" + settings.getVisualMaterializationSettings().getDetachedVisualInterestRefreshIntervalTicks() - + " candidateInterval=" + settings.getVisualMaterializationSettings().getDetachedVisualCandidateRefreshIntervalTicks() - + " visibilityInterval=" + settings.getVisualMaterializationSettings().getDetachedVisualVisibilityCheckIntervalTicks() - + " spawnRate=" + settings.getVisualMaterializationSettings().getDetachedVisualMaxSpawnsPerTick() - + " cap=" + settings.getVisualMaterializationSettings().getDetachedVisualMaxMaterialized() - + " blockType=" + settings.getVisualMaterializationSettings().getDetachedVisualBlockType())); + + ": enabled=" + settings.isDetachedVisualMaterializationEnabled() + + " materializationRadius=" + settings.getDetachedVisualMaterializationRadius() + + " dematerializationRadius=" + settings.getDetachedVisualDematerializationRadius() + + " interestInterval=" + settings.getDetachedVisualInterestRefreshIntervalTicks() + + " candidateInterval=" + settings.getDetachedVisualCandidateRefreshIntervalTicks() + + " visibilityInterval=" + settings.getDetachedVisualVisibilityCheckIntervalTicks() + + " spawnRate=" + settings.getDetachedVisualMaxSpawnsPerTick() + + " cap=" + settings.getDetachedVisualMaxMaterialized() + + " blockType=" + settings.getDetachedVisualBlockType())); } private static Boolean parseBoolean(@Nonnull String value) { diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/VisualSettingsCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/commands/VisualSettingsCommand.java similarity index 84% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/VisualSettingsCommand.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/commands/VisualSettingsCommand.java index 71df9875..718d9f23 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/VisualSettingsCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/commands/VisualSettingsCommand.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.commands.settings; +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.commands; import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection; diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/VisualSyncSettingsCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/commands/VisualSyncSettingsCommand.java similarity index 78% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/VisualSyncSettingsCommand.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/commands/VisualSyncSettingsCommand.java index 5681d3dd..c3267913 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/commands/settings/VisualSyncSettingsCommand.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/commands/VisualSyncSettingsCommand.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.commands.settings; +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.commands; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; @@ -10,12 +10,13 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.commands.SpaceSelection; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsVisualSyncSettings; -import dev.hytalemodding.impulse.core.plugin.settings.VisualOcclusionMode; +import dev.hytalemodding.impulse.core.internal.commands.space.SpaceSelection; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsSpaces; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualSyncSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.VisualOcclusionMode; import java.util.Locale; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; @@ -110,14 +111,22 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); - SpaceId spaceId = SpaceSelection.resolve(ctx, world, resource, spaceArg); - if (spaceId == null) { + Store physicsStore = PhysicsThreading.store(world); + SpaceSelection.SelectedSpace selectedSpace = SpaceSelection.resolveStoreSpace(ctx, + world, + spaceArg); + if (selectedSpace == null) { return CompletableFuture.completedFuture(null); } + SpaceId spaceId = selectedSpace.spaceId(); - PhysicsSpaceSettings settings = new PhysicsSpaceSettings( - resource.getSpaceSettings(spaceId)); + PhysicsVisualSyncSettings settings = PhysicsSpaces.visualSyncSettings(physicsStore, + selectedSpace.spaceRef()); + if (settings == null) { + ctx.sender().sendMessage(Message.raw("Physics space id=" + spaceId.value() + + " no longer exists.")); + return CompletableFuture.completedFuture(null); + } if (!anyArgProvided(ctx)) { sendSummary(ctx, spaceId, settings); return CompletableFuture.completedFuture(null); @@ -125,10 +134,10 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, int fullRadius = fullRadiusArg.provided(ctx) ? fullRadiusArg.get(ctx) - : settings.getVisualSyncSettings().getVisualFullSyncRadius(); + : settings.getVisualFullSyncRadius(); int maxRadius = maxRadiusArg.provided(ctx) ? maxRadiusArg.get(ctx) - : settings.getVisualSyncSettings().getVisualMaxSyncRadius(); + : settings.getVisualMaxSyncRadius(); if (outOfRange(fullRadius, PhysicsVisualSyncSettings.MAX_VISUAL_FULL_SYNC_RADIUS) || outOfRange(maxRadius, PhysicsVisualSyncSettings.MAX_VISUAL_MAX_SYNC_RADIUS) || fullRadius > maxRadius) { @@ -194,7 +203,7 @@ && smoothingRateOutOfRange(smoothingRateArg.get(ctx), ctx.sender().sendMessage(Message.raw("farMode must be cutoff or lod.")); return CompletableFuture.completedFuture(null); } - settings.getVisualSyncSettings().setVisualFarSyncCutoffEnabled(cutoff); + settings.setVisualFarSyncCutoffEnabled(cutoff); } if (occlusionArg.provided(ctx)) { VisualOcclusionMode occlusionMode = parseOcclusionMode(occlusionArg.get(ctx)); @@ -202,7 +211,7 @@ && smoothingRateOutOfRange(smoothingRateArg.get(ctx), ctx.sender().sendMessage(Message.raw("occlusion must be off, priority, or cull.")); return CompletableFuture.completedFuture(null); } - settings.getVisualSyncSettings().setVisualOcclusionMode(occlusionMode); + settings.setVisualOcclusionMode(occlusionMode); } if (entityCullingArg.provided(ctx)) { Boolean entityCulling = parseBoolean(entityCullingArg.get(ctx)); @@ -210,7 +219,7 @@ && smoothingRateOutOfRange(smoothingRateArg.get(ctx), ctx.sender().sendMessage(Message.raw("entityCulling must be true or false.")); return CompletableFuture.completedFuture(null); } - settings.getVisualSyncSettings().setEntityVisualSyncCullingEnabled(entityCulling); + settings.setEntityVisualSyncCullingEnabled(entityCulling); } if (visibilityCullingArg.provided(ctx)) { Boolean visibilityCulling = parseBoolean(visibilityCullingArg.get(ctx)); @@ -218,7 +227,7 @@ && smoothingRateOutOfRange(smoothingRateArg.get(ctx), ctx.sender().sendMessage(Message.raw("visibilityCulling must be true or false.")); return CompletableFuture.completedFuture(null); } - settings.getVisualSyncSettings().setVisualVisibilityCullingEnabled(visibilityCulling); + settings.setVisualVisibilityCullingEnabled(visibilityCulling); } if (predictionArg.provided(ctx)) { Boolean prediction = parseBoolean(predictionArg.get(ctx)); @@ -226,7 +235,7 @@ && smoothingRateOutOfRange(smoothingRateArg.get(ctx), ctx.sender().sendMessage(Message.raw("prediction must be true or false.")); return CompletableFuture.completedFuture(null); } - settings.getVisualSyncSettings().setVisualSnapshotPredictionEnabled(prediction); + settings.setVisualSnapshotPredictionEnabled(prediction); } if (smoothingArg.provided(ctx)) { Boolean smoothing = parseBoolean(smoothingArg.get(ctx)); @@ -234,29 +243,29 @@ && smoothingRateOutOfRange(smoothingRateArg.get(ctx), ctx.sender().sendMessage(Message.raw("smoothing must be true or false.")); return CompletableFuture.completedFuture(null); } - settings.getVisualSyncSettings().setVisualSnapshotSmoothingEnabled(smoothing); + settings.setVisualSnapshotSmoothingEnabled(smoothing); } - settings.getVisualSyncSettings().setVisualSyncRadii(fullRadius, maxRadius); + settings.setVisualSyncRadii(fullRadius, maxRadius); if (midIntervalArg.provided(ctx)) { - settings.getVisualSyncSettings().setVisualMidSyncIntervalTicks(midIntervalArg.get(ctx)); + settings.setVisualMidSyncIntervalTicks(midIntervalArg.get(ctx)); } if (farIntervalArg.provided(ctx)) { - settings.getVisualSyncSettings().setVisualFarSyncIntervalTicks(farIntervalArg.get(ctx)); + settings.setVisualFarSyncIntervalTicks(farIntervalArg.get(ctx)); } if (occlusionRaycastsArg.provided(ctx)) { - settings.getVisualSyncSettings().setVisualOcclusionRaycastsPerTick(occlusionRaycastsArg.get(ctx)); + settings.setVisualOcclusionRaycastsPerTick(occlusionRaycastsArg.get(ctx)); } if (occlusionCacheArg.provided(ctx)) { - settings.getVisualSyncSettings().setVisualOcclusionCacheTicks(occlusionCacheArg.get(ctx)); + settings.setVisualOcclusionCacheTicks(occlusionCacheArg.get(ctx)); } if (predictionMaxSecondsArg.provided(ctx)) { - settings.getVisualSyncSettings().setVisualSnapshotPredictionMaxSeconds(predictionMaxSecondsArg.get(ctx)); + settings.setVisualSnapshotPredictionMaxSeconds(predictionMaxSecondsArg.get(ctx)); } if (smoothingRateArg.provided(ctx)) { - settings.getVisualSyncSettings().setVisualSnapshotSmoothingRate(smoothingRateArg.get(ctx)); + settings.setVisualSnapshotSmoothingRate(smoothingRateArg.get(ctx)); } - resource.setSpaceSettings(spaceId, settings); + PhysicsSpaces.putVisualSyncSettings(physicsStore, selectedSpace.spaceRef(), settings); sendSummary(ctx, spaceId, settings); return CompletableFuture.completedFuture(null); } @@ -292,23 +301,23 @@ private static boolean smoothingRateOutOfRange(float value, float maxValue) { private static void sendSummary(@Nonnull CommandContext ctx, @Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceSettings settings) { + @Nonnull PhysicsVisualSyncSettings settings) { ctx.sender().sendMessage(Message.raw("Impulse visual sync settings for space " + spaceId.value() - + ": fullRadius=" + settings.getVisualSyncSettings().getVisualFullSyncRadius() - + " maxRadius=" + settings.getVisualSyncSettings().getVisualMaxSyncRadius() - + " farMode=" + (settings.getVisualSyncSettings().isVisualFarSyncCutoffEnabled() ? "cutoff" : "lod") - + " midInterval=" + settings.getVisualSyncSettings().getVisualMidSyncIntervalTicks() - + " farInterval=" + settings.getVisualSyncSettings().getVisualFarSyncIntervalTicks() - + " occlusion=" + settings.getVisualSyncSettings().getVisualOcclusionMode().name().toLowerCase(Locale.ROOT) - + " occlusionRaycasts=" + settings.getVisualSyncSettings().getVisualOcclusionRaycastsPerTick() - + " occlusionCache=" + settings.getVisualSyncSettings().getVisualOcclusionCacheTicks() - + " prediction=" + settings.getVisualSyncSettings().isVisualSnapshotPredictionEnabled() - + " predictionMaxSeconds=" + settings.getVisualSyncSettings().getVisualSnapshotPredictionMaxSeconds() - + " smoothing=" + settings.getVisualSyncSettings().isVisualSnapshotSmoothingEnabled() - + " smoothingRate=" + settings.getVisualSyncSettings().getVisualSnapshotSmoothingRate() - + " entityCulling=" + settings.getVisualSyncSettings().isEntityVisualSyncCullingEnabled() - + " visibilityCulling=" + settings.getVisualSyncSettings().isVisualVisibilityCullingEnabled())); + + ": fullRadius=" + settings.getVisualFullSyncRadius() + + " maxRadius=" + settings.getVisualMaxSyncRadius() + + " farMode=" + (settings.isVisualFarSyncCutoffEnabled() ? "cutoff" : "lod") + + " midInterval=" + settings.getVisualMidSyncIntervalTicks() + + " farInterval=" + settings.getVisualFarSyncIntervalTicks() + + " occlusion=" + settings.getVisualOcclusionMode().name().toLowerCase(Locale.ROOT) + + " occlusionRaycasts=" + settings.getVisualOcclusionRaycastsPerTick() + + " occlusionCache=" + settings.getVisualOcclusionCacheTicks() + + " prediction=" + settings.isVisualSnapshotPredictionEnabled() + + " predictionMaxSeconds=" + settings.getVisualSnapshotPredictionMaxSeconds() + + " smoothing=" + settings.isVisualSnapshotSmoothingEnabled() + + " smoothingRate=" + settings.getVisualSnapshotSmoothingRate() + + " entityCulling=" + settings.isEntityVisualSyncCullingEnabled() + + " visibilityCulling=" + settings.isVisualVisibilityCullingEnabled())); } private static Boolean parseFarCutoff(@Nonnull String value) { diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyRuntimeState.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/resources/PhysicsBodyRuntimeState.java similarity index 51% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyRuntimeState.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/resources/PhysicsBodyRuntimeState.java index 1774b4ea..23c6274f 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyRuntimeState.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/resources/PhysicsBodyRuntimeState.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.resources.body; +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; @@ -7,6 +7,7 @@ import org.joml.Quaternionf; import org.joml.Vector3f; import java.util.Map; +import java.util.UUID; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -44,18 +45,30 @@ public static final class BodySyncState { private final Quaternionf lastSyncedRotation = new Quaternionf(); @Nonnull private final Vector3f lastObservedSnapshotPosition = new Vector3f(); + @Nonnull + private final Quaternionf lastObservedSnapshotRotation = new Quaternionf(); @Getter private boolean initialized; @Getter private boolean sleeping; @Getter private boolean snapshotObserved; + @Nullable + private UUID lastSyncedBodyUuid; @Getter private float secondsSinceSync; public void recordSync(@Nonnull Vector3f position, @Nonnull Quaternionf rotation, boolean sleeping) { + recordSync(null, position, rotation, sleeping); + } + + public void recordSync(@Nullable UUID bodyUuid, + @Nonnull Vector3f position, + @Nonnull Quaternionf rotation, + boolean sleeping) { + lastSyncedBodyUuid = bodyUuid; lastSyncedPosition.set(position); lastSyncedRotation.set(rotation); initialized = true; @@ -63,6 +76,23 @@ public void recordSync(@Nonnull Vector3f position, secondsSinceSync = 0.0f; } + public boolean isInitializedFor(@Nonnull UUID bodyUuid) { + return initialized + && (lastSyncedBodyUuid == null || lastSyncedBodyUuid.equals(bodyUuid)); + } + + public void clear() { + initialized = false; + sleeping = false; + snapshotObserved = false; + lastSyncedBodyUuid = null; + secondsSinceSync = 0.0f; + lastSyncedPosition.zero(); + lastSyncedRotation.identity(); + lastObservedSnapshotPosition.zero(); + lastObservedSnapshotRotation.identity(); + } + public float recordSnapshotObservation(@Nonnull Vector3f position) { if (!snapshotObserved) { lastObservedSnapshotPosition.set(position); @@ -74,6 +104,23 @@ public float recordSnapshotObservation(@Nonnull Vector3f position) { return distance; } + @Nonnull + public SnapshotMotion recordSnapshotObservation(@Nonnull Vector3f position, + @Nonnull Quaternionf rotation) { + if (!snapshotObserved) { + lastObservedSnapshotPosition.set(position); + lastObservedSnapshotRotation.set(rotation); + snapshotObserved = true; + return SnapshotMotion.unobserved(); + } + float distance = lastObservedSnapshotPosition.distance(position); + float rotationRadians = rotationDistanceRadians(lastObservedSnapshotRotation, + rotation); + lastObservedSnapshotPosition.set(position); + lastObservedSnapshotRotation.set(rotation); + return new SnapshotMotion(distance, rotationRadians); + } + public void recordSkip(float dt) { secondsSinceSync += Math.max(dt, 0.0f); } @@ -93,5 +140,28 @@ public Vector3f getLastObservedSnapshotPosition() { return lastObservedSnapshotPosition; } + private static float rotationDistanceRadians(@Nonnull Quaternionf first, + @Nonnull Quaternionf second) { + float dot = Math.abs(first.x * second.x + + first.y * second.y + + first.z * second.z + + first.w * second.w); + return 2.0f * (float) Math.acos(Math.min(dot, 1.0f)); + } + + public record SnapshotMotion(float positionDistance, + float rotationRadians) { + + @Nonnull + private static SnapshotMotion unobserved() { + return new SnapshotMotion(Float.NaN, Float.NaN); + } + + public boolean observed() { + return !Float.isNaN(positionDistance) + && !Float.isNaN(rotationRadians); + } + } + } } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/resources/PhysicsBodySyncStateResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/resources/PhysicsBodySyncStateResource.java new file mode 100644 index 00000000..004e205f --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/resources/PhysicsBodySyncStateResource.java @@ -0,0 +1,65 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources.PhysicsBodyRuntimeState.BodySyncState; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; +import java.util.Map; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Runtime-only EntityStore sync state for body-to-entity transform projection. + */ +public final class PhysicsBodySyncStateResource implements Resource { + + @Nullable + private static ResourceType resourceType; + + private final Map, BodySyncState> bodySyncStates = + new Reference2ObjectOpenHashMap<>(); + + @Nonnull + public synchronized BodySyncState getOrCreate(@Nonnull Ref entityRef) { + return bodySyncStates.computeIfAbsent(entityRef, _ -> new BodySyncState()); + } + + @Nullable + public synchronized BodySyncState get(@Nonnull Ref entityRef) { + return bodySyncStates.get(entityRef); + } + + public synchronized void clear(@Nonnull Ref entityRef) { + bodySyncStates.remove(entityRef); + } + + public synchronized void clear() { + bodySyncStates.clear(); + } + + @Nonnull + @Override + public PhysicsBodySyncStateResource clone() { + PhysicsBodySyncStateResource copy = new PhysicsBodySyncStateResource(); + synchronized (this) { + copy.bodySyncStates.putAll(bodySyncStates); + } + return copy; + } + + @Nullable + public static ResourceType getResourceType() { + return resourceType; + } + + public static void setResourceType( + @Nonnull ResourceType type) { + resourceType = type; + } + + public static void clearResourceType() { + resourceType = null; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/resources/PhysicsDebugOverlayResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/resources/PhysicsDebugOverlayResource.java new file mode 100644 index 00000000..1385f865 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/resources/PhysicsDebugOverlayResource.java @@ -0,0 +1,164 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources; + +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import java.util.Set; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.Getter; + +/** + * Runtime-only debug overlay projection state for one world EntityStore. + * + *

Physics debug flags live on PhysicsStore. This resource owns only viewer subscription, + * projection cadence, and packet-budget state for rendering copied physics debug views.

+ */ +@Getter +public class PhysicsDebugOverlayResource implements Resource { + + @Getter + @Nullable + private static ResourceType resourceType; + + public static final float MIN_REFRESH_SECONDS = 0.05f; + public static final float MAX_REFRESH_SECONDS = 2.0f; + public static final float DEFAULT_OVERLAY_REFRESH_SECONDS = 0.10f; + public static final float DEFAULT_PHYSICS_CHUNK_REFRESH_SECONDS = 0.25f; + + public static final double DEFAULT_VIEW_RADIUS = 96.0; + public static final int DEFAULT_MAX_BODIES = 512; + public static final int DEFAULT_MAX_CONTACTS = 384; + public static final int DEFAULT_MAX_JOINTS = 384; + public static final int DEFAULT_MAX_PHYSICS_CHUNK_SECTIONS = 192; + public static final int DEFAULT_MAX_PHYSICS_CHUNK_BOXES = 768; + + private final Set subscriberUuids = new ObjectOpenHashSet<>(); + + private float overlayRefreshSeconds = DEFAULT_OVERLAY_REFRESH_SECONDS; + private float physicsChunkRefreshSeconds = DEFAULT_PHYSICS_CHUNK_REFRESH_SECONDS; + private float overlayTimeUntilRefresh; + private float physicsChunkTimeUntilRefresh; + + private double viewRadius = DEFAULT_VIEW_RADIUS; + private int maxBodies = DEFAULT_MAX_BODIES; + private int maxContacts = DEFAULT_MAX_CONTACTS; + private int maxJoints = DEFAULT_MAX_JOINTS; + private int maxPhysicsChunkSections = DEFAULT_MAX_PHYSICS_CHUNK_SECTIONS; + private int maxPhysicsChunkBoxes = DEFAULT_MAX_PHYSICS_CHUNK_BOXES; + + public PhysicsDebugOverlayResource() { + } + + public boolean addSubscriber(@Nonnull UUID uuid) { + return subscriberUuids.add(uuid); + } + + public boolean removeSubscriber(@Nonnull UUID uuid) { + return subscriberUuids.remove(uuid); + } + + public void clearSubscribers() { + subscriberUuids.clear(); + } + + @Nonnull + public Set getSubscriberUuids() { + return Set.copyOf(subscriberUuids); + } + + public boolean hasSubscribers() { + return !subscriberUuids.isEmpty(); + } + + public void setOverlayRefreshSeconds(float overlayRefreshSeconds) { + this.overlayRefreshSeconds = clampRefresh(overlayRefreshSeconds); + } + + public void setPhysicsChunkRefreshSeconds(float physicsChunkRefreshSeconds) { + this.physicsChunkRefreshSeconds = clampRefresh(physicsChunkRefreshSeconds); + } + + public void setViewRadius(double viewRadius) { + this.viewRadius = Math.max(1.0, viewRadius); + } + + public void setMaxBodies(int maxBodies) { + this.maxBodies = Math.max(1, maxBodies); + } + + public void setMaxContacts(int maxContacts) { + this.maxContacts = Math.max(1, maxContacts); + } + + public void setMaxJoints(int maxJoints) { + this.maxJoints = Math.max(1, maxJoints); + } + + public void setMaxPhysicsChunkSections(int maxPhysicsChunkSections) { + this.maxPhysicsChunkSections = Math.max(1, maxPhysicsChunkSections); + } + + public void setMaxPhysicsChunkBoxes(int maxPhysicsChunkBoxes) { + this.maxPhysicsChunkBoxes = Math.max(1, maxPhysicsChunkBoxes); + } + + public boolean tickOverlayBudget(float dt) { + overlayTimeUntilRefresh -= dt; + if (overlayTimeUntilRefresh > 0.0f) { + return false; + } + + overlayTimeUntilRefresh += overlayRefreshSeconds; + if (overlayTimeUntilRefresh <= 0.0f) { + overlayTimeUntilRefresh = overlayRefreshSeconds; + } + return true; + } + + public boolean tickPhysicsChunkBudget(float dt) { + physicsChunkTimeUntilRefresh -= dt; + if (physicsChunkTimeUntilRefresh > 0.0f) { + return false; + } + + physicsChunkTimeUntilRefresh += physicsChunkRefreshSeconds; + if (physicsChunkTimeUntilRefresh <= 0.0f) { + physicsChunkTimeUntilRefresh = physicsChunkRefreshSeconds; + } + return true; + } + + @Nonnull + @Override + public PhysicsDebugOverlayResource clone() { + PhysicsDebugOverlayResource copy = new PhysicsDebugOverlayResource(); + copy.subscriberUuids.addAll(subscriberUuids); + copy.overlayRefreshSeconds = overlayRefreshSeconds; + copy.physicsChunkRefreshSeconds = physicsChunkRefreshSeconds; + copy.overlayTimeUntilRefresh = overlayTimeUntilRefresh; + copy.physicsChunkTimeUntilRefresh = physicsChunkTimeUntilRefresh; + copy.viewRadius = viewRadius; + copy.maxBodies = maxBodies; + copy.maxContacts = maxContacts; + copy.maxJoints = maxJoints; + copy.maxPhysicsChunkSections = maxPhysicsChunkSections; + copy.maxPhysicsChunkBoxes = maxPhysicsChunkBoxes; + return copy; + } + + public static void setResourceType( + @Nonnull ResourceType type) { + resourceType = type; + } + + public static void clearResourceType() { + resourceType = null; + } + + private static float clampRefresh(float value) { + return Math.clamp(value, MIN_REFRESH_SECONDS, MAX_REFRESH_SECONDS); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsBodyAttachmentIndexSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsBodyAttachmentIndexSystem.java new file mode 100644 index 00000000..fe389ef5 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsBodyAttachmentIndexSystem.java @@ -0,0 +1,145 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.sync; + +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.RefChangeSystem; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsProjectionIndexResource; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent.AttachmentLifecycle; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Keeps the body-key to entity-ref attachment index in sync with ECS component changes. + */ +public class PhysicsBodyAttachmentIndexSystem + extends RefChangeSystem { + + @Nonnull + private final ComponentType attachmentType; + @Nonnull + private final Query query; + + public PhysicsBodyAttachmentIndexSystem() { + this(BodyAttachmentComponent.getComponentType()); + } + + PhysicsBodyAttachmentIndexSystem( + @Nonnull ComponentType attachmentType) { + this.attachmentType = Objects.requireNonNull(attachmentType, "attachmentType"); + this.query = attachmentType; + } + + @Override + public void onComponentAdded(@Nonnull Ref ref, + @Nonnull BodyAttachmentComponent component, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + registerAttachment(ref, component, commandBuffer); + } + + @Override + public void onComponentSet(@Nonnull Ref ref, + @Nullable BodyAttachmentComponent oldComponent, + @Nonnull BodyAttachmentComponent newComponent, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + assert oldComponent != null; + updateAttachment(ref, oldComponent, newComponent, commandBuffer); + } + + @Override + public void onComponentRemoved(@Nonnull Ref ref, + @Nonnull BodyAttachmentComponent component, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + unregisterAttachment(ref, component, commandBuffer); + } + + private static void updateAttachment(@Nonnull Ref ref, + @Nonnull BodyAttachmentComponent oldComponent, + @Nonnull BodyAttachmentComponent newComponent, + @Nonnull CommandBuffer commandBuffer) { + UUID oldUuid = oldComponent.getBodyUuid(); + UUID newUuid = newComponent.getBodyUuid(); + Ref oldBodyRef = oldComponent.getBodyRef(); + Ref newBodyRef = newComponent.getBodyRef(); + boolean sameUuid = oldUuid.equals(newUuid); + boolean sameBodyRef = sameRef(oldBodyRef, newBodyRef); + boolean oldGeneratedProxy = oldComponent.getLifecycle() == AttachmentLifecycle.GENERATED_PROXY; + boolean newGeneratedProxy = newComponent.getLifecycle() == AttachmentLifecycle.GENERATED_PROXY; + if (sameUuid && sameBodyRef && oldGeneratedProxy == newGeneratedProxy) { + return; + } + PhysicsProjectionIndexResource resource = commandBuffer.getResource( + PhysicsProjectionIndexResource.getResourceType()); + if (!sameUuid) { + resource.unregisterAttachment(oldUuid, oldBodyRef, ref); + resource.registerAttachment(newUuid, newBodyRef, ref); + } else if (!sameBodyRef) { + resource.updateAttachmentBodyRef(newUuid, + oldBodyRef, + newBodyRef, + ref, + newGeneratedProxy); + } + if (oldGeneratedProxy) { + resource.clearGeneratedVisualProxy(oldUuid, oldBodyRef, ref); + } + if (newGeneratedProxy) { + resource.setGeneratedVisualProxy(newUuid, newBodyRef, ref); + } + } + + private static void registerAttachment(@Nonnull Ref ref, + @Nonnull BodyAttachmentComponent component, + @Nonnull CommandBuffer commandBuffer) { + UUID bodyUuid = component.getBodyUuid(); + PhysicsProjectionIndexResource resource = commandBuffer.getResource( + PhysicsProjectionIndexResource.getResourceType()); + resource.registerAttachment(bodyUuid, component.getBodyRef(), ref); + if (component.getLifecycle() == AttachmentLifecycle.GENERATED_PROXY) { + resource.setGeneratedVisualProxy(bodyUuid, component.getBodyRef(), ref); + } + } + + private static void unregisterAttachment(@Nonnull Ref ref, + @Nonnull BodyAttachmentComponent component, + @Nonnull CommandBuffer commandBuffer) { + UUID bodyUuid = component.getBodyUuid(); + PhysicsProjectionIndexResource resource = commandBuffer.getResource( + PhysicsProjectionIndexResource.getResourceType()); + resource.unregisterAttachment(bodyUuid, component.getBodyRef(), ref); + if (component.getLifecycle() == AttachmentLifecycle.GENERATED_PROXY) { + resource.clearGeneratedVisualProxy(bodyUuid, component.getBodyRef(), ref); + } + } + + private static boolean sameRef(@Nullable Ref first, + @Nullable Ref second) { + return first == second + || first != null + && second != null + && first.getStore() == second.getStore() + && first.getIndex() == second.getIndex(); + } + + @Nonnull + @Override + public ComponentType componentType() { + return attachmentType; + } + + @Nonnull + @Override + public Query getQuery() { + return query; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsSyncPolicy.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsSyncPolicy.java similarity index 70% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsSyncPolicy.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsSyncPolicy.java index 32ffbb62..c414640a 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsSyncPolicy.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsSyncPolicy.java @@ -1,10 +1,9 @@ -package dev.hytalemodding.impulse.core.internal.systems.sync; +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.sync; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRuntimeState; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources.PhysicsBodyRuntimeState; import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime.BodyVisualInterestState; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsVisualSyncSettings; -import dev.hytalemodding.impulse.core.plugin.settings.VisualOcclusionMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualSyncSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.VisualOcclusionMode; import java.util.List; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -16,16 +15,11 @@ */ public final class PhysicsSyncPolicy { - // Near visuals sync after roughly one thirty-second of a block of movement. + // Near sleeping visuals still sync if the backend pose changes while asleep. private static final float POSITION_SYNC_THRESHOLD = 1.0f / 32.0f; private static final float POSITION_SYNC_THRESHOLD_SQUARED = POSITION_SYNC_THRESHOLD * POSITION_SYNC_THRESHOLD; - // Low-speed visuals use a wider deadzone for tiny awake-body solver jitter. - private static final float LOW_SPEED_POSITION_SYNC_THRESHOLD = 1.0f / 8.0f; - private static final float LOW_SPEED_POSITION_SYNC_THRESHOLD_SQUARED = - LOW_SPEED_POSITION_SYNC_THRESHOLD * LOW_SPEED_POSITION_SYNC_THRESHOLD; - // Mid-range visuals are outside the full-sync radius but still within visual range. private static final float MID_RANGE_POSITION_SYNC_THRESHOLD = 0.5f; private static final float MID_RANGE_POSITION_SYNC_THRESHOLD_SQUARED = @@ -38,8 +32,6 @@ public final class PhysicsSyncPolicy { */ private static final float ROTATION_SYNC_DOT_THRESHOLD = (float) Math.cos(Math.toRadians(1.0)); - private static final float LOW_SPEED_ROTATION_SYNC_DOT_THRESHOLD = - (float) Math.cos(Math.toRadians(3.0)); private static final float MID_RANGE_ROTATION_SYNC_DOT_THRESHOLD = (float) Math.cos(Math.toRadians(8.0)); @@ -53,7 +45,6 @@ public final class PhysicsSyncPolicy { // Keepalive updates bound how long an awake visual can stay below sync thresholds. private static final float ACTIVE_KEEPALIVE_SECONDS = 0.25f; - private static final float LOW_SPEED_KEEPALIVE_SECONDS = 1.25f; private static final float MID_RANGE_KEEPALIVE_SECONDS = 2.5f; private static final float SECONDS_PER_TICK = 0.05f; @@ -61,13 +52,13 @@ private PhysicsSyncPolicy() { } @Nonnull - static SyncRangeTier resolveRangeTier(@Nullable PhysicsSpaceSettings settings, + static SyncRangeTier resolveRangeTier(@Nullable PhysicsVisualSyncSettings settings, @Nullable BodyVisualInterestState visualInterestState, boolean rangeLimitedVisual, - boolean controlled, + boolean kinematic, @Nonnull List playerInterests, @Nonnull Vector3f visualPosition) { - if (!rangeLimitedVisual || controlled) { + if (!rangeLimitedVisual || kinematic) { return SyncRangeTier.NEAR; } if (playerInterests.isEmpty()) { @@ -76,18 +67,18 @@ static SyncRangeTier resolveRangeTier(@Nullable PhysicsSpaceSettings settings, if (settings == null) { return SyncRangeTier.NEAR; } - if (settings.getVisualSyncSettings().getVisualOcclusionMode() == VisualOcclusionMode.CULL + if (settings.getVisualOcclusionMode() == VisualOcclusionMode.CULL && visualInterestState != null - && visualInterestState.hasFreshRaycast(settings.getVisualSyncSettings().getVisualOcclusionCacheTicks()) + && visualInterestState.hasFreshRaycast(settings.getVisualOcclusionCacheTicks()) && !visualInterestState.isRaycastVisible()) { // Materialization owns the raycast budget; sync only consumes fresh CULL results. return SyncRangeTier.FAR; } - float fullRadiusSquared = square(settings.getVisualSyncSettings().getVisualFullSyncRadius()); - float maxRadiusSquared = square(settings.getVisualSyncSettings().getVisualMaxSyncRadius()); + float fullRadiusSquared = square(settings.getVisualFullSyncRadius()); + float maxRadiusSquared = square(settings.getVisualMaxSyncRadius()); float nearestDistanceSquared = Float.MAX_VALUE; - boolean visibilityCulling = settings.getVisualSyncSettings().isVisualVisibilityCullingEnabled(); + boolean visibilityCulling = settings.isVisualVisibilityCullingEnabled(); for (PlayerInterest playerInterest : playerInterests) { float distanceSquared = playerInterest.position().distanceSquared(visualPosition); if (distanceSquared <= fullRadiusSquared @@ -103,12 +94,11 @@ static SyncRangeTier resolveRangeTier(@Nullable PhysicsSpaceSettings settings, @Nonnull static SyncDecision resolveSyncDecision(@Nonnull PhysicsBodyRuntimeState.BodySyncState syncState, - @Nullable PhysicsSpaceSettings settings, + @Nullable PhysicsVisualSyncSettings settings, @Nonnull Vector3f position, @Nonnull Quaternionf rotation, boolean sleeping, - boolean lowSpeed, - boolean controlled, + boolean kinematic, @Nonnull SyncRangeTier rangeTier) { if (!syncState.isInitialized()) { return SyncDecision.INITIAL; @@ -116,8 +106,9 @@ static SyncDecision resolveSyncDecision(@Nonnull PhysicsBodyRuntimeState.BodySyn if (sleeping != syncState.isSleeping()) { return SyncDecision.TRANSITION; } + PhysicsVisualSyncSettings visualSyncSettings = settingsOrDefault(settings); if (rangeTier == SyncRangeTier.FAR - && (settings == null || settings.getVisualSyncSettings().isVisualFarSyncCutoffEnabled())) { + && visualSyncSettings.isVisualFarSyncCutoffEnabled()) { return SyncDecision.SKIP_VISUAL_RANGE; } if (sleeping && rangeTier != SyncRangeTier.NEAR) { @@ -128,25 +119,20 @@ static SyncDecision resolveSyncDecision(@Nonnull PhysicsBodyRuntimeState.BodySyn float rotationDotThreshold; float keepaliveSeconds; int minimumIntervalTicks = 1; - if (rangeTier == SyncRangeTier.FAR && !controlled) { + if (rangeTier == SyncRangeTier.FAR && !kinematic) { positionThresholdSquared = MID_RANGE_POSITION_SYNC_THRESHOLD_SQUARED; rotationDotThreshold = MID_RANGE_ROTATION_SYNC_DOT_THRESHOLD; - keepaliveSeconds = intervalSeconds(settings.getVisualSyncSettings().getVisualFarSyncIntervalTicks()); - minimumIntervalTicks = settings.getVisualSyncSettings().getVisualFarSyncIntervalTicks(); - } else if (rangeTier == SyncRangeTier.MID && !controlled) { + keepaliveSeconds = intervalSeconds(visualSyncSettings.getVisualFarSyncIntervalTicks()); + minimumIntervalTicks = visualSyncSettings.getVisualFarSyncIntervalTicks(); + } else if (rangeTier == SyncRangeTier.MID && !kinematic) { positionThresholdSquared = MID_RANGE_POSITION_SYNC_THRESHOLD_SQUARED; rotationDotThreshold = MID_RANGE_ROTATION_SYNC_DOT_THRESHOLD; keepaliveSeconds = MID_RANGE_KEEPALIVE_SECONDS; - minimumIntervalTicks = settings != null - ? settings.getVisualSyncSettings().getVisualMidSyncIntervalTicks() - : PhysicsVisualSyncSettings.DEFAULT_VISUAL_MID_SYNC_INTERVAL_TICKS; + minimumIntervalTicks = visualSyncSettings.getVisualMidSyncIntervalTicks(); } else { - positionThresholdSquared = lowSpeed && !controlled - ? LOW_SPEED_POSITION_SYNC_THRESHOLD_SQUARED : POSITION_SYNC_THRESHOLD_SQUARED; - rotationDotThreshold = lowSpeed && !controlled - ? LOW_SPEED_ROTATION_SYNC_DOT_THRESHOLD : ROTATION_SYNC_DOT_THRESHOLD; - keepaliveSeconds = lowSpeed && !controlled - ? LOW_SPEED_KEEPALIVE_SECONDS : ACTIVE_KEEPALIVE_SECONDS; + positionThresholdSquared = POSITION_SYNC_THRESHOLD_SQUARED; + rotationDotThreshold = ROTATION_SYNC_DOT_THRESHOLD; + keepaliveSeconds = ACTIVE_KEEPALIVE_SECONDS; } if (minimumIntervalTicks > 1 @@ -154,6 +140,11 @@ static SyncDecision resolveSyncDecision(@Nonnull PhysicsBodyRuntimeState.BodySyn return SyncDecision.SKIP_VISUAL_RANGE; } + if (!sleeping && rangeTier == SyncRangeTier.NEAR) { + // Smooth near awake visuals prefer cadence; sleep and range tiers own throttling. + return SyncDecision.THRESHOLD; + } + if (position.distanceSquared(syncState.getLastSyncedPosition()) >= positionThresholdSquared || rotationChangedEnough(rotation, syncState.getLastSyncedRotation(), rotationDotThreshold)) { return SyncDecision.THRESHOLD; @@ -167,8 +158,7 @@ static SyncDecision resolveSyncDecision(@Nonnull PhysicsBodyRuntimeState.BodySyn if (rangeTier == SyncRangeTier.MID) { return SyncDecision.SKIP_VISUAL_RANGE; } - return lowSpeed && !controlled ? SyncDecision.SKIP_VISUAL_DEADZONE - : SyncDecision.SKIP_THRESHOLD; + return SyncDecision.SKIP_THRESHOLD; } private static boolean rotationChangedEnough(@Nonnull Quaternionf current, @@ -205,17 +195,23 @@ private static float intervalSeconds(int ticks) { return ticks * SECONDS_PER_TICK; } - static float visualPredictionSeconds(@Nullable PhysicsSpaceSettings settings, + static float visualPredictionSeconds(@Nullable PhysicsVisualSyncSettings settings, long currentNanos, long snapshotAppliedNanos) { if (settings == null - || !settings.getVisualSyncSettings().isVisualSnapshotPredictionEnabled() + || !settings.isVisualSnapshotPredictionEnabled() || currentNanos <= snapshotAppliedNanos || snapshotAppliedNanos <= 0L) { return 0.0f; } float ageSeconds = (currentNanos - snapshotAppliedNanos) / 1_000_000_000.0f; - return Math.min(ageSeconds, settings.getVisualSyncSettings().getVisualSnapshotPredictionMaxSeconds()); + return Math.min(ageSeconds, settings.getVisualSnapshotPredictionMaxSeconds()); + } + + @Nonnull + private static PhysicsVisualSyncSettings settingsOrDefault( + @Nullable PhysicsVisualSyncSettings settings) { + return settings != null ? settings : new PhysicsVisualSyncSettings(); } public record PlayerInterest(@Nonnull Vector3f position, @Nonnull Vector3f direction) { diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsSyncSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsSyncSystem.java new file mode 100644 index 00000000..ea76c7c6 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsSyncSystem.java @@ -0,0 +1,494 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.sync; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.dependency.SystemGroupDependency; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.tick.EntityTickingSystem; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.modules.entity.system.TransformSystems; +import com.hypixel.hytale.server.core.modules.entity.system.UpdateLocationSystems; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.core.internal.math.PhysicsVisualPoseMath; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources.PhysicsBodySyncStateResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsProjectionIndexResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources.PhysicsBodyRuntimeState; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.visual.PhysicsProjectionCleanupSystem; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.visual.VisualInterestCollector; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsEntityTypes; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent.AttachmentLifecycle; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.VisualSyncSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualSyncSettings; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +/** + * Synchronizes physics bodies with Hytale transforms each tick. + * + *

Runs after the persistence restore group so that newly bootstrapped spaces, + * hydrated bodies and hydrated joints are all settled before this system reads + * body transforms.

+ * + *

Entities attach to authoritative PhysicsStore body UUIDs. Backend body destruction is explicit + * through PhysicsStore entities; removing an EntityStore attachment only removes the projection.

+ */ +public class PhysicsSyncSystem extends EntityTickingSystem { + + private static final float TRANSFORM_POSITION_EPSILON = 0.000001f; + private static final float TRANSFORM_ROTATION_EPSILON = 0.000001f; + + @Nonnull + private final ComponentType attachmentType; + @Nonnull + private final ComponentType transformType; + @Nonnull + private final Query query; + private final Set> dependencies = Set.of( + new SystemGroupDependency<>(Order.AFTER, PhysicsEntityTypes.persistenceRestoreGroup()), + new SystemDependency<>(Order.AFTER, PhysicsProjectionCleanupSystem.class), + new SystemDependency<>(Order.BEFORE, TransformSystems.EntityTrackerUpdate.class), + new SystemDependency<>(Order.BEFORE, UpdateLocationSystems.TickingSystem.class) + ); + + @Nonnull + private final ThreadLocal syncTickContext = new ThreadLocal<>(); + + /** + * Hytale may run entity ticks in parallel. Each tick task needs independent temporary objects + * because the backend out-parameter getters write into caller-owned vectors. + */ + private final ThreadLocal scratch = ThreadLocal.withInitial(Scratch::new); + + public PhysicsSyncSystem() { + this(BodyAttachmentComponent.getComponentType(), TransformComponent.getComponentType()); + } + + PhysicsSyncSystem( + @Nonnull ComponentType attachmentType, + @Nonnull ComponentType transformType) { + this.attachmentType = Objects.requireNonNull(attachmentType, "attachmentType"); + this.transformType = Objects.requireNonNull(transformType, "transformType"); + this.query = Query.and(attachmentType, transformType); + } + + @Override + public boolean isParallel(int archetypeChunkSize, int taskCount) { + // Backend bodies and per-body sync state are owned by the world tick thread. + // Body poses are read from the world-level snapshot cache before parallel ECS writes. + return false; + } + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + assert PhysicsRuntimeProfilingResource.getResourceType() != null; + PhysicsRuntimeProfilingResource profiling = store.getResource( + PhysicsRuntimeProfilingResource.getResourceType()); + PhysicsRuntimeProfilingResource.SyncCollector collector = profiling.isEnabled() + ? profiling.beginSyncSample() : null; + long startNanos = collector != null ? System.nanoTime() : 0L; + try { + syncTickContext.set(collectSyncTickContext(store)); + super.tick(dt, systemIndex, store); + } finally { + if (collector != null) { + profiling.finishSyncSample(collector, System.nanoTime() - startNanos); + } + syncTickContext.remove(); + } + } + + @Override + public void tick(float dt, + int index, + @Nonnull ArchetypeChunk chunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer) { + Ref entityRef = chunk.getReferenceTo(index); + BodyAttachmentComponent attachment = chunk.getComponent(index, attachmentType); + TransformComponent transform = chunk.getComponent(index, transformType); + if (attachment == null || transform == null) { + return; + } + + Scratch local = scratch.get(); + PhysicsRuntimeProfilingResource.SyncCollector collector = local.getSyncCollector(store); + if (collector != null) { + collector.incrementBodiesInspected(); + } + SyncTickContext context = syncTickContext.get(); + PhysicsBodySnapshot physicsStoreSnapshot = resolvePhysicsStoreSnapshot(entityRef, + attachment, + context.snapshotResource(), + store); + if (physicsStoreSnapshot != null) { + if (!PhysicsTransformAuthority.shouldApplyBodyTransform(attachment)) { + if (collector != null) { + collector.incrementSkippedStatic(); + } + return; + } + PhysicsBodyRuntimeState.BodySyncState syncState = store.getResource( + PhysicsBodySyncStateResource.getResourceType()) + .getOrCreate(entityRef); + PhysicsVisualSyncSettings settings = + context.visualSyncSettings(physicsStoreSnapshot.spaceUuid()); + computeVisualPose(attachment, physicsStoreSnapshot, local); + boolean kinematic = physicsStoreSnapshot.bodyType() == PhysicsBodyType.KINEMATIC; + SyncResult result = applyComputedPhysicsStoreSnapshot(transform, + physicsStoreSnapshot, + local, + syncState, + settings, + resolveRangeTier(settings, + attachment, + context.playerInterests(), + local.visualPosition, + kinematic), + kinematic, + dt); + if (collector != null) { + recordSyncResult(collector, result); + } + return; + } + if (collector != null) { + collector.incrementSkippedMissingSpace(); + } + } + + @Nullable + private static PhysicsBodySnapshot resolvePhysicsStoreSnapshot( + @Nonnull Ref entityRef, + @Nonnull BodyAttachmentComponent attachment, + @Nonnull PhysicsSnapshotResource snapshotResource, + @Nonnull Store store) { + Ref oldBodyRef = attachment.getBodyRef(); + PhysicsBodySnapshot snapshot = null; + if (oldBodyRef != null && oldBodyRef.isValid()) { + snapshot = snapshotResource.getBody(oldBodyRef); + if (snapshot != null && !snapshot.bodyUuid().equals(attachment.getBodyUuid())) { + snapshot = null; + } + } + if (snapshot == null) { + snapshot = snapshotResource.getBody(attachment.getBodyUuid()); + } + Ref newBodyRef = snapshot != null ? snapshot.bodyRef() : null; + if (!sameRef(oldBodyRef, newBodyRef)) { + store.getResource(PhysicsProjectionIndexResource.getResourceType()) + .updateAttachmentBodyRef(attachment.getBodyUuid(), + oldBodyRef, + newBodyRef, + entityRef, + attachment.getLifecycle() == AttachmentLifecycle.GENERATED_PROXY); + attachment.setBodyRef(newBodyRef); + } + return snapshot; + } + + private static boolean sameRef(@Nullable Ref first, + @Nullable Ref second) { + return first == second + || (first != null + && second != null + && first.getStore() == second.getStore() + && first.getIndex() == second.getIndex()); + } + + @Nonnull + private static SyncTickContext collectSyncTickContext( + @Nonnull Store store) { + Store physics = PhysicsThreading.store(store.getExternalData().getWorld()); + PhysicsThreading.requireWorldThread(physics, + "read copied PhysicsStore sync snapshots"); + return new SyncTickContext(physics, + physics.getResource(PhysicsSnapshotResource.getResourceType()), + VisualInterestCollector.collectSyncInterests(store)); + } + + static boolean applyPhysicsStoreSnapshot(@Nonnull TransformComponent transform, + @Nonnull BodyAttachmentComponent attachment, + @Nonnull PhysicsBodySnapshot snapshot, + @Nonnull Scratch scratch) { + computeVisualPose(attachment, snapshot, scratch); + return writeTransformIfChanged(transform, scratch); + } + + @Nonnull + static SyncResult applyPhysicsStoreSnapshot(@Nonnull TransformComponent transform, + @Nonnull BodyAttachmentComponent attachment, + @Nonnull PhysicsBodySnapshot snapshot, + @Nonnull Scratch scratch, + @Nonnull PhysicsBodyRuntimeState.BodySyncState syncState, + @Nullable PhysicsVisualSyncSettings settings, + @Nonnull PhysicsSyncPolicy.SyncRangeTier rangeTier, + float dt) { + computeVisualPose(attachment, snapshot, scratch); + return applyComputedPhysicsStoreSnapshot(transform, + snapshot, + scratch, + syncState, + settings, + rangeTier, + false, + dt); + } + + @Nonnull + private static SyncResult applyComputedPhysicsStoreSnapshot( + @Nonnull TransformComponent transform, + @Nonnull PhysicsBodySnapshot snapshot, + @Nonnull Scratch scratch, + @Nonnull PhysicsBodyRuntimeState.BodySyncState syncState, + @Nullable PhysicsVisualSyncSettings settings, + @Nonnull PhysicsSyncPolicy.SyncRangeTier rangeTier, + boolean kinematic, + float dt) { + if (!syncState.isInitializedFor(snapshot.bodyUuid())) { + syncState.clear(); + } + PhysicsSyncPolicy.SyncDecision decision = PhysicsSyncPolicy.resolveSyncDecision(syncState, + settings, + scratch.visualPosition, + scratch.visualRotation, + snapshot.sleeping(), + kinematic, + rangeTier); + if (!shouldWriteTransform(decision)) { + syncState.recordSkip(dt); + return new SyncResult(decision, false); + } + boolean changed = writeTransformIfChanged(transform, scratch); + syncState.recordSync(snapshot.bodyUuid(), + scratch.visualPosition, + scratch.visualRotation, + snapshot.sleeping()); + return new SyncResult(decision, changed); + } + + private static void computeVisualPose(@Nonnull BodyAttachmentComponent attachment, + @Nonnull PhysicsBodySnapshot snapshot, + @Nonnull Scratch scratch) { + scratch.position.set(snapshot.positionX(), snapshot.positionY(), snapshot.positionZ()); + setFiniteUnitQuaternionOrIdentity(scratch.rotation, + snapshot.rotationX(), + snapshot.rotationY(), + snapshot.rotationZ(), + snapshot.rotationW()); + PhysicsVisualPoseMath.visualPositionFromBodyPose(scratch.position, + scratch.rotation, + attachment.resolveVisualOriginOffsetY(snapshot.centerOfMassOffsetY()), + attachment.getLocalPositionOffset(), + scratch.visualPosition, + scratch.worldOffset); + scratch.visualRotation.set(scratch.rotation); + setFiniteUnitQuaternionOrIdentity(scratch.localRotationOffset, + attachment.getLocalRotationOffset()); + scratch.visualRotation.mul(scratch.localRotationOffset); + setFiniteUnitQuaternionOrIdentity(scratch.visualRotation, scratch.visualRotation); + scratch.visualRotation.getEulerAnglesYXZ(scratch.euler); + if (!isFinite(scratch.euler)) { + scratch.euler.zero(); + } + } + + private static boolean writeTransformIfChanged(@Nonnull TransformComponent transform, + @Nonnull Scratch scratch) { + if (matchesTransform(transform, scratch.visualPosition, scratch.euler)) { + return false; + } + transform.getPosition().set(scratch.visualPosition.x, + scratch.visualPosition.y, + scratch.visualPosition.z); + transform.getRotation().set(scratch.euler.x, scratch.euler.y, scratch.euler.z); + return true; + } + + private static boolean shouldWriteTransform( + @Nonnull PhysicsSyncPolicy.SyncDecision decision) { + return decision == PhysicsSyncPolicy.SyncDecision.INITIAL + || decision == PhysicsSyncPolicy.SyncDecision.THRESHOLD + || decision == PhysicsSyncPolicy.SyncDecision.TRANSITION + || decision == PhysicsSyncPolicy.SyncDecision.KEEPALIVE; + } + + @Nonnull + private static PhysicsSyncPolicy.SyncRangeTier resolveRangeTier( + @Nullable PhysicsVisualSyncSettings settings, + @Nonnull BodyAttachmentComponent attachment, + @Nonnull List playerInterests, + @Nonnull Vector3f visualPosition, + boolean kinematic) { + boolean generatedProxy = attachment.getLifecycle() == AttachmentLifecycle.GENERATED_PROXY; + boolean rangeLimitedVisual = generatedProxy + || settings != null && settings.isEntityVisualSyncCullingEnabled(); + return PhysicsSyncPolicy.resolveRangeTier(settings, + null, + rangeLimitedVisual, + kinematic, + playerInterests, + visualPosition); + } + + private static void recordSyncResult( + @Nonnull PhysicsRuntimeProfilingResource.SyncCollector collector, + @Nonnull SyncResult result) { + if (result.transformChanged()) { + collector.incrementBodiesSynced(); + } + switch (result.decision()) { + case TRANSITION -> collector.incrementTransitionSyncs(); + case KEEPALIVE -> collector.incrementKeepaliveSyncs(); + case SKIP_SLEEPING -> collector.incrementSkippedSleeping(); + case SKIP_THRESHOLD -> collector.incrementSkippedThreshold(); + case SKIP_VISUAL_DEADZONE -> collector.incrementSkippedVisualDeadzone(); + case SKIP_VISUAL_RANGE -> collector.incrementSkippedVisualRange(); + default -> { + } + } + } + + private static boolean matchesTransform(@Nonnull TransformComponent transform, + @Nonnull Vector3f position, + @Nonnull Vector3f rotation) { + return Math.abs(transform.getPosition().x - position.x) <= TRANSFORM_POSITION_EPSILON + && Math.abs(transform.getPosition().y - position.y) <= TRANSFORM_POSITION_EPSILON + && Math.abs(transform.getPosition().z - position.z) <= TRANSFORM_POSITION_EPSILON + && Math.abs(transform.getRotation().x() - rotation.x) <= TRANSFORM_ROTATION_EPSILON + && Math.abs(transform.getRotation().y() - rotation.y) <= TRANSFORM_ROTATION_EPSILON + && Math.abs(transform.getRotation().z() - rotation.z) <= TRANSFORM_ROTATION_EPSILON; + } + + private static void setFiniteUnitQuaternionOrIdentity(@Nonnull Quaternionf target, + float x, + float y, + float z, + float w) { + target.set(x, y, z, w); + setFiniteUnitQuaternionOrIdentity(target, target); + } + + private static void setFiniteUnitQuaternionOrIdentity(@Nonnull Quaternionf target, + @Nonnull Quaternionf source) { + if (target != source) { + target.set(source); + } + if (!isFiniteAndNonZero(target)) { + target.identity(); + return; + } + target.normalize(); + if (!isFiniteAndNonZero(target)) { + target.identity(); + } + } + + private static boolean isFiniteAndNonZero(@Nonnull Quaternionf quaternion) { + float lengthSquared = quaternion.x * quaternion.x + + quaternion.y * quaternion.y + + quaternion.z * quaternion.z + + quaternion.w * quaternion.w; + return Float.isFinite(lengthSquared) && lengthSquared > 0.0f; + } + + private static boolean isFinite(@Nonnull Vector3f vector) { + return Float.isFinite(vector.x) + && Float.isFinite(vector.y) + && Float.isFinite(vector.z); + } + + static final class Scratch { + + private final Vector3f position = new Vector3f(); + private final Quaternionf rotation = new Quaternionf(); + private final Quaternionf localRotationOffset = new Quaternionf(); + private final Vector3f visualPosition = new Vector3f(); + private final Quaternionf visualRotation = new Quaternionf(); + private final Vector3f worldOffset = new Vector3f(); + private final Vector3f euler = new Vector3f(); + + @Nullable + private Store cachedProfilingStore; + @Nullable + private PhysicsRuntimeProfilingResource cachedProfiling; + + @Nullable + private PhysicsRuntimeProfilingResource.SyncCollector getSyncCollector( + @Nonnull Store store) { + if (cachedProfilingStore != store || cachedProfiling == null) { + cachedProfilingStore = store; + cachedProfiling = store.getResource(PhysicsRuntimeProfilingResource.getResourceType()); + } + return cachedProfiling.isEnabled() ? cachedProfiling.getActiveSyncCollector() : null; + } + } + + record SyncResult(@Nonnull PhysicsSyncPolicy.SyncDecision decision, + boolean transformChanged) { + } + + private record SyncTickContext(@Nonnull Store physicsStore, + @Nonnull PhysicsSnapshotResource snapshotResource, + @Nonnull List playerInterests, + @Nonnull Map settingsBySpace) { + + private SyncTickContext(@Nonnull Store physicsStore, + @Nonnull PhysicsSnapshotResource snapshotResource, + @Nonnull List playerInterests) { + this(physicsStore, snapshotResource, playerInterests, new HashMap<>()); + } + + @Nullable + private PhysicsVisualSyncSettings visualSyncSettings(@Nonnull UUID spaceUuid) { + if (settingsBySpace.containsKey(spaceUuid)) { + return settingsBySpace.get(spaceUuid); + } + Ref spaceRef = physicsStore.getExternalData().getRefFromUUID(spaceUuid); + PhysicsVisualSyncSettings settings = null; + if (spaceRef != null && spaceRef.getStore() == physicsStore && spaceRef.isValid()) { + VisualSyncSettingsComponent component = physicsStore.getComponent(spaceRef, + VisualSyncSettingsComponent.getComponentType()); + settings = new PhysicsVisualSyncSettings(); + if (component != null) { + component.copyTo(settings); + } + } + settingsBySpace.put(spaceUuid, settings); + return settings; + } + } + + @Nonnull + @Override + public Query getQuery() { + return query; + } + + @Nonnull + @Override + public Set> getDependencies() { + return dependencies; + } + +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsTransformAuthority.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsTransformAuthority.java new file mode 100644 index 00000000..6c1b325f --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsTransformAuthority.java @@ -0,0 +1,15 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.sync; + +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent.TransformAuthority; +import javax.annotation.Nonnull; + +final class PhysicsTransformAuthority { + + private PhysicsTransformAuthority() { + } + + static boolean shouldApplyBodyTransform(@Nonnull BodyAttachmentComponent attachment) { + return attachment.getTransformAuthority() == TransformAuthority.BODY; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/GeneratedProxyLifecycle.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/visual/GeneratedProxyLifecycle.java similarity index 58% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/GeneratedProxyLifecycle.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/visual/GeneratedProxyLifecycle.java index 18094dfd..53f951b2 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/GeneratedProxyLifecycle.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/visual/GeneratedProxyLifecycle.java @@ -1,17 +1,18 @@ -package dev.hytalemodding.impulse.core.internal.systems.visual; +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.visual; import com.hypixel.hytale.component.CommandBuffer; import com.hypixel.hytale.component.ComponentAccessor; -import com.hypixel.hytale.component.ComponentType; import com.hypixel.hytale.component.Holder; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.RemoveReason; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent.AttachmentLifecycle; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources.PhysicsBodySyncStateResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsProjectionIndexResource; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent.AttachmentLifecycle; +import java.util.UUID; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -20,43 +21,39 @@ */ public final class GeneratedProxyLifecycle { - private static final ComponentType ATTACHMENT_TYPE = - PhysicsBodyAttachmentComponent.getComponentType(); - private GeneratedProxyLifecycle() { } static void removeProxy(@Nonnull ComponentAccessor accessor, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull RigidBodyKey bodyKey) { - Ref proxy = resource.getGeneratedVisualProxy(bodyKey); - removeProxy(accessor, resource, bodyKey, proxy); - } - - static void removeProxy(@Nonnull ComponentAccessor accessor, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull RigidBodyKey bodyKey, + @Nonnull PhysicsProjectionIndexResource projection, + @Nonnull UUID bodyUuid, + @Nullable Ref bodyRef, @Nullable Ref proxy) { if (proxy == null) { - resource.clearGeneratedVisualProxy(bodyKey); + projection.clearGeneratedVisualProxyForBodyRef(bodyUuid, bodyRef); } else { - resource.clearGeneratedVisualProxy(bodyKey, proxy); + projection.clearGeneratedVisualProxy(bodyUuid, bodyRef, proxy); } removeEntity(accessor, proxy); } public static void clearMissingAttachment(@Nonnull Ref entityRef, - @Nonnull PhysicsBodyAttachmentComponent attachment, - @Nonnull PhysicsWorldRuntimeResource resource, + @Nonnull BodyAttachmentComponent attachment, @Nonnull CommandBuffer commandBuffer) { - resource.unregisterBodyAttachment(attachment.getBodyKey(), entityRef); - resource.clearBodySyncState(entityRef); + UUID bodyUuid = attachment.getBodyUuid(); + assert PhysicsProjectionIndexResource.getResourceType() != null; + PhysicsProjectionIndexResource projection = commandBuffer.getResource( + PhysicsProjectionIndexResource.getResourceType()); + projection.unregisterAttachment(bodyUuid, attachment.getBodyRef(), entityRef); + commandBuffer.getResource(PhysicsBodySyncStateResource.getResourceType()) + .clear(entityRef); if (attachment.getLifecycle() == AttachmentLifecycle.GENERATED_PROXY) { - removeProxy(commandBuffer, resource, attachment.getBodyKey(), entityRef); + projection.clearGeneratedVisualProxy(bodyUuid, attachment.getBodyRef(), entityRef); + removeEntity(commandBuffer, entityRef); } else if (attachment.shouldRemoveEntityWhenBodyMissing()) { removeEntity(commandBuffer, entityRef); } else { - commandBuffer.removeComponent(entityRef, ATTACHMENT_TYPE); + commandBuffer.removeComponent(entityRef, BodyAttachmentComponent.getComponentType()); } } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/visual/PhysicsProjectionCleanupSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/visual/PhysicsProjectionCleanupSystem.java new file mode 100644 index 00000000..ebd53973 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/visual/PhysicsProjectionCleanupSystem.java @@ -0,0 +1,116 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.visual; + +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.RemoveReason; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemGroupDependency; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsEntityTypes; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent.AttachmentLifecycle; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.GeneratedVisualProxyComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Cleans EntityStore projections whose authoritative PhysicsStore row is gone. + */ +public class PhysicsProjectionCleanupSystem extends TickingSystem { + + private static final int CLEANUP_INTERVAL_TICKS = 40; + + private final Set> dependencies = Set.of( + new SystemGroupDependency<>(Order.AFTER, PhysicsEntityTypes.persistenceRestoreGroup()) + ); + @Nonnull + private final Map, Integer> cleanupCooldowns = + Collections.synchronizedMap(new WeakHashMap<>()); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + clearDestroyedBodyAttachments(store); + if (shouldSkipCleanup(store)) { + return; + } + removeOrphanGeneratedVisualProxyMarkers(store); + } + + private boolean shouldSkipCleanup(@Nonnull Store store) { + synchronized (cleanupCooldowns) { + int cooldown = cleanupCooldowns.getOrDefault(store, 0); + if (cooldown > 0) { + cleanupCooldowns.put(store, cooldown - 1); + return true; + } + cleanupCooldowns.put(store, CLEANUP_INTERVAL_TICKS); + return false; + } + } + + private static void clearDestroyedBodyAttachments(@Nonnull Store store) { + ComponentType attachmentType = + BodyAttachmentComponent.getComponentType(); + Store physicsStore = PhysicsThreading.storeOrNull( + store.getExternalData().getWorld()); + store.forEachEntityParallel(attachmentType, + (index, archetypeChunk, commandBuffer) -> { + BodyAttachmentComponent attachment = archetypeChunk.getComponent(index, + attachmentType); + if (attachment == null || !hasMissingBody(physicsStore, attachment)) { + return; + } + GeneratedProxyLifecycle.clearMissingAttachment(archetypeChunk.getReferenceTo(index), + attachment, + commandBuffer); + }); + } + + private static boolean hasDestroyedBodyRef(@Nonnull BodyAttachmentComponent attachment) { + Ref bodyRef = attachment.getBodyRef(); + return bodyRef != null && !bodyRef.isValid(); + } + + private static boolean hasMissingBody(@Nullable Store store, + @Nonnull BodyAttachmentComponent attachment) { + if (hasDestroyedBodyRef(attachment)) { + return true; + } + if (attachment.getBodyRef() != null + || attachment.getLifecycle() != AttachmentLifecycle.GENERATED_PROXY) { + return false; + } + return store != null + && !PhysicsBodies.isRegistered(store, attachment.getBodyUuid()); + } + + private static void removeOrphanGeneratedVisualProxyMarkers(@Nonnull Store store) { + ComponentType attachmentType = + BodyAttachmentComponent.getComponentType(); + ComponentType generatedProxyType = + GeneratedVisualProxyComponent.getComponentType(); + store.forEachEntityParallel(generatedProxyType, + (index, archetypeChunk, commandBuffer) -> { + if (archetypeChunk.getComponent(index, attachmentType) != null) { + return; + } + commandBuffer.removeEntity(archetypeChunk.getReferenceTo(index), RemoveReason.REMOVE); + }); + } + + @Nonnull + @Override + public Set> getDependencies() { + return dependencies; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/VisualInterestCollector.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/visual/VisualInterestCollector.java similarity index 91% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/VisualInterestCollector.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/visual/VisualInterestCollector.java index d8c8cf25..2aaf50ee 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/VisualInterestCollector.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/visual/VisualInterestCollector.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.systems.visual; +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.visual; import com.hypixel.hytale.component.ComponentType; import com.hypixel.hytale.component.Ref; @@ -9,9 +9,9 @@ import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualInterestResource; import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime; -import dev.hytalemodding.impulse.core.internal.systems.sync.PhysicsSyncPolicy; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.sync.PhysicsSyncPolicy; import java.util.ArrayList; import java.util.List; import javax.annotation.Nonnull; @@ -57,8 +57,7 @@ public static List collectSyncInterests( @Nonnull public static List collectMaterializationInterests( - @Nonnull Store store, - @Nonnull PhysicsWorldRuntimeResource resource) { + @Nonnull Store store) { List interests = new ArrayList<>(); for (PlayerRef playerRef : store.getExternalData().getWorld().getPlayerRefs()) { Ref playerEntity = playerRef.getReference(); @@ -76,7 +75,8 @@ public static List collectMaterializationIn new Vector3f((float) position.x, (float) position.y, (float) position.z), playerLookDirection(store, playerEntity, transform))); } - interests.addAll(resource.getSyntheticVisualInterests()); + interests.addAll(store.getResource(PhysicsVisualInterestResource.getResourceType()) + .getSyntheticVisualInterests()); return interests; } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/PhysicsChunkBoundaryRuntime.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/PhysicsChunkBoundaryRuntime.java deleted file mode 100644 index 1eaea764..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/PhysicsChunkBoundaryRuntime.java +++ /dev/null @@ -1,244 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Map; -import java.util.Set; -import java.util.function.Consumer; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import lombok.Getter; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -/** - * Chunk-boundary and forced-CCD runtime state for registered bodies. - */ -public final class PhysicsChunkBoundaryRuntime { - - private final Set forcedContinuousCollisionBodyKeys = new ObjectOpenHashSet<>(); - private final Map chunkBoundarySafeStates = - new Object2ObjectOpenHashMap<>(); - private final Map chunkBoundaryPauseStates = - new Object2ObjectOpenHashMap<>(); - - public void updateChunkBoundarySafeState(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f position, - @Nonnull Quaternionf rotation) { - ChunkBoundarySafeState state = chunkBoundarySafeStates.computeIfAbsent(bodyKey, - ignored -> new ChunkBoundarySafeState()); - state.set(position, rotation); - } - - public void updateChunkBoundarySafeState(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodySnapshot snapshot) { - ChunkBoundarySafeState state = chunkBoundarySafeStates.computeIfAbsent(bodyKey, - ignored -> new ChunkBoundarySafeState()); - state.set(snapshot); - } - - @Nullable - public ChunkBoundarySafeState getChunkBoundarySafeState( - @Nonnull RigidBodyKey bodyKey) { - return chunkBoundarySafeStates.get(bodyKey); - } - - public void pauseChunkBoundaryBody(@Nonnull RigidBodyKey bodyKey, - long targetChunkIndex, - @Nonnull PhysicsBodyType originalBodyType, - @Nonnull Vector3f linearVelocity, - @Nonnull Vector3f angularVelocity) { - ChunkBoundaryPauseState state = chunkBoundaryPauseStates.computeIfAbsent(bodyKey, - ignored -> new ChunkBoundaryPauseState()); - state.set(targetChunkIndex, originalBodyType, linearVelocity, angularVelocity); - } - - public void pauseChunkBoundaryBody(@Nonnull RigidBodyKey bodyKey, - long targetChunkIndex, - @Nonnull long[] targetChunkIndices, - @Nonnull PhysicsBodySnapshot snapshot) { - ChunkBoundaryPauseState state = chunkBoundaryPauseStates.computeIfAbsent(bodyKey, - ignored -> new ChunkBoundaryPauseState()); - state.set(targetChunkIndex, targetChunkIndices, snapshot); - } - - public void pauseChunkBoundaryBody(@Nonnull RigidBodyKey bodyKey, - long targetChunkIndex, - @Nonnull PhysicsBodySnapshot snapshot) { - ChunkBoundaryPauseState state = chunkBoundaryPauseStates.computeIfAbsent(bodyKey, - ignored -> new ChunkBoundaryPauseState()); - state.set(targetChunkIndex, snapshot); - } - - @Nullable - public ChunkBoundaryPauseState getChunkBoundaryPauseState( - @Nonnull RigidBodyKey bodyKey) { - return chunkBoundaryPauseStates.get(bodyKey); - } - - public void clearChunkBoundaryPauseState(@Nonnull RigidBodyKey bodyKey) { - chunkBoundaryPauseStates.remove(bodyKey); - } - - @Nonnull - public Collection getChunkBoundaryPausedBodyKeys() { - return new ArrayList<>(chunkBoundaryPauseStates.keySet()); - } - - public void markContinuousCollisionForced(@Nonnull RigidBodyKey bodyKey) { - forcedContinuousCollisionBodyKeys.add(bodyKey); - } - - @Nonnull - public Collection getForcedContinuousCollisionBodyKeys() { - return new ArrayList<>(forcedContinuousCollisionBodyKeys); - } - - public boolean hasForcedContinuousCollisionBodies() { - return !forcedContinuousCollisionBodyKeys.isEmpty(); - } - - public void forEachForcedContinuousCollisionBody(@Nonnull Consumer consumer) { - forcedContinuousCollisionBodyKeys.forEach(consumer); - } - - public void clearForcedContinuousCollisionBodies() { - forcedContinuousCollisionBodyKeys.clear(); - } - - public void clearBody(@Nonnull RigidBodyKey bodyKey) { - forcedContinuousCollisionBodyKeys.remove(bodyKey); - chunkBoundarySafeStates.remove(bodyKey); - chunkBoundaryPauseStates.remove(bodyKey); - } - - public void clear() { - forcedContinuousCollisionBodyKeys.clear(); - chunkBoundarySafeStates.clear(); - chunkBoundaryPauseStates.clear(); - } - - public void clearChunkBoundaryStates() { - chunkBoundarySafeStates.clear(); - chunkBoundaryPauseStates.clear(); - } - - public static final class ChunkBoundaryPauseState { - - @Getter - private long targetChunkIndex; - @Nonnull - private long[] targetChunkIndices = new long[0]; - @Nonnull - private PhysicsBodyType originalBodyType = PhysicsBodyType.DYNAMIC; - @Nonnull - private final Vector3f linearVelocity = new Vector3f(); - @Nonnull - private final Vector3f angularVelocity = new Vector3f(); - - public void set(long targetChunkIndex, - @Nonnull PhysicsBodyType originalBodyType, - @Nonnull Vector3f linearVelocity, - @Nonnull Vector3f angularVelocity) { - this.targetChunkIndex = targetChunkIndex; - this.targetChunkIndices = new long[] {targetChunkIndex}; - this.originalBodyType = originalBodyType; - this.linearVelocity.set(linearVelocity); - this.angularVelocity.set(angularVelocity); - } - - public void set(long targetChunkIndex, - @Nonnull long[] targetChunkIndices, - @Nonnull PhysicsBodySnapshot snapshot) { - this.targetChunkIndex = targetChunkIndex; - this.targetChunkIndices = copyTargetChunkIndices(targetChunkIndex, targetChunkIndices); - this.originalBodyType = snapshot.bodyType(); - snapshot.copyLinearVelocityTo(this.linearVelocity); - snapshot.copyAngularVelocityTo(this.angularVelocity); - } - - public void set(long targetChunkIndex, @Nonnull PhysicsBodySnapshot snapshot) { - this.targetChunkIndex = targetChunkIndex; - this.targetChunkIndices = new long[] {targetChunkIndex}; - this.originalBodyType = snapshot.bodyType(); - snapshot.copyLinearVelocityTo(this.linearVelocity); - snapshot.copyAngularVelocityTo(this.angularVelocity); - } - - @Nonnull - public PhysicsBodyType getOriginalBodyType() { - return originalBodyType; - } - - @Nonnull - public Vector3f getLinearVelocity() { - return linearVelocity; - } - - @Nonnull - public Vector3f getAngularVelocity() { - return angularVelocity; - } - - @Nonnull - public long[] getTargetChunkIndices() { - return targetChunkIndices.clone(); - } - - @Nonnull - private static long[] copyTargetChunkIndices(long targetChunkIndex, - @Nonnull long[] targetChunkIndices) { - if (targetChunkIndices.length == 0) { - return new long[] {targetChunkIndex}; - } - long[] copy = targetChunkIndices.clone(); - if (copy[0] != targetChunkIndex) { - int targetIndex = -1; - for (int index = 1; index < copy.length; index++) { - if (copy[index] == targetChunkIndex) { - targetIndex = index; - break; - } - } - if (targetIndex >= 0) { - copy[targetIndex] = copy[0]; - } - copy[0] = targetChunkIndex; - } - return copy; - } - } - - public static final class ChunkBoundarySafeState { - - @Nonnull - private final Vector3f position = new Vector3f(); - @Nonnull - private final Quaternionf rotation = new Quaternionf(); - - public void set(@Nonnull Vector3f position, @Nonnull Quaternionf rotation) { - this.position.set(position); - this.rotation.set(rotation); - } - - public void set(@Nonnull PhysicsBodySnapshot snapshot) { - snapshot.copyPositionTo(this.position); - snapshot.copyRotationTo(this.rotation); - } - - @Nonnull - public Vector3f getPosition() { - return position; - } - - @Nonnull - public Quaternionf getRotation() { - return rotation; - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/PhysicsWorldCollisionRuntime.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/PhysicsWorldCollisionRuntime.java deleted file mode 100644 index d3c0774f..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/PhysicsWorldCollisionRuntime.java +++ /dev/null @@ -1,185 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision; - -import com.hypixel.hytale.server.core.universe.world.World; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionBuildStats; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionPrewarmStats; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionStats; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldCollisionSettings; -import it.unimi.dsi.fastutil.ints.Int2LongMap; -import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap; -import it.unimi.dsi.fastutil.longs.LongOpenHashSet; -import it.unimi.dsi.fastutil.longs.LongSet; -import java.util.Objects; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3d; - -/** - * World-collision runtime state for one physics world. - */ -public final class PhysicsWorldCollisionRuntime { - - private final WorldVoxelCollisionCache worldVoxelCollisionCache = new WorldVoxelCollisionCache(); - private final Int2LongMap streamingRevisions = new Int2LongOpenHashMap(); - - @Nonnull - public WorldVoxelCollisionCache worldVoxelCollisionCache() { - return worldVoxelCollisionCache; - } - - public synchronized void registerSpace(@Nonnull SpaceId spaceId) { - streamingRevisions.putIfAbsent(spaceId.value(), 1L); - } - - public synchronized void unregisterSpace(@Nonnull SpaceId spaceId) { - streamingRevisions.remove(spaceId.value()); - } - - public synchronized long streamingRevision(@Nonnull SpaceId spaceId) { - return streamingRevisions.getOrDefault(spaceId.value(), 0L); - } - - public synchronized long incrementStreamingRevision(@Nonnull SpaceId spaceId) { - long revision = streamingRevisions.getOrDefault(spaceId.value(), 0L) + 1L; - streamingRevisions.put(spaceId.value(), revision); - return revision; - } - - @Nonnull - public WorldCollisionBuildStats rebuildAround(@Nonnull World world, - @Nonnull PhysicsSpaceBinding space, - @Nonnull Vector3d center, - int radius) { - return rebuildAround(world, - space, - center, - radius, - WorldCollisionBuildOptions.fromNativeVoxelTerrainEnabled( - PhysicsWorldCollisionSettings.DEFAULT_NATIVE_VOXEL_TERRAIN_ENABLED)); - } - - @Nonnull - public WorldCollisionBuildStats rebuildAround(@Nonnull World world, - @Nonnull PhysicsSpaceBinding space, - @Nonnull Vector3d center, - int radius, - @Nonnull WorldCollisionBuildOptions buildOptions) { - return worldCollisionStats(worldVoxelCollisionCache.rebuildAround(world, - space, - center, - radius, - buildOptions)); - } - - @Nonnull - public WorldCollisionBuildStats refreshAround(@Nonnull World world, - @Nonnull PhysicsSpaceBinding space, - @Nonnull Vector3d center, - int radius, - @Nonnull WorldCollisionBuildOptions buildOptions) { - WorldVoxelCollisionCache.BuildStats stats = worldVoxelCollisionCache.refreshAround(world, - space, - center, - radius, - buildOptions); - if (stats.removedBodies() > 0) { - incrementStreamingRevision(space.spaceId()); - } - return worldCollisionStats(stats); - } - - @Nonnull - public WorldCollisionPrewarmStats ensureAround(@Nonnull World world, - @Nonnull PhysicsSpaceBinding space, - @Nonnull Iterable centers, - int radius, - long tick) { - return ensureAround(world, - space, - centers, - radius, - tick, - WorldCollisionBuildOptions.fromNativeVoxelTerrainEnabled( - PhysicsWorldCollisionSettings.DEFAULT_NATIVE_VOXEL_TERRAIN_ENABLED)); - } - - @Nonnull - public WorldCollisionPrewarmStats ensureAround(@Nonnull World world, - @Nonnull PhysicsSpaceBinding space, - @Nonnull Iterable centers, - int radius, - long tick, - @Nonnull WorldCollisionBuildOptions buildOptions) { - Objects.requireNonNull(centers, "centers"); - LongSet visitedSections = new LongOpenHashSet(); - WorldVoxelCollisionCache.BuildStats total = WorldVoxelCollisionCache.BuildStats.empty(); - for (Vector3d center : centers) { - total = total.plus(worldVoxelCollisionCache.ensureAround(world, - space, - center, - radius, - tick, - null, - visitedSections, - null, - null, - buildOptions)); - } - return new WorldCollisionPrewarmStats(visitedSections.size(), - worldCollisionStats(total)); - } - - public int clear(@Nonnull PhysicsSpaceBinding space) { - incrementStreamingRevision(space.spaceId()); - return worldVoxelCollisionCache.clear(space); - } - - public void clear(@Nonnull SpaceId spaceId, @Nullable PhysicsSpaceBinding space) { - worldVoxelCollisionCache.clear(spaceId, space); - unregisterSpace(spaceId); - } - - public synchronized void clearAll() { - worldVoxelCollisionCache.copyFrom(new WorldVoxelCollisionCache()); - for (int spaceId : streamingRevisions.keySet().toIntArray()) { - streamingRevisions.put(spaceId, streamingRevisions.get(spaceId) + 1L); - } - } - - public void clearRetainedTerrain(@Nonnull Iterable spaces) { - for (PhysicsSpaceBinding space : spaces) { - clear(space); - } - worldVoxelCollisionCache.finishStreamingApply(); - } - - public synchronized void clearAllAndUnregisterSpaces() { - worldVoxelCollisionCache.copyFrom(new WorldVoxelCollisionCache()); - streamingRevisions.clear(); - } - - @Nonnull - public WorldCollisionStats getStats() { - return new WorldCollisionStats(worldVoxelCollisionCache.spaceCount(), - worldVoxelCollisionCache.sectionCount(), - worldVoxelCollisionCache.bodyCount(), - worldVoxelCollisionCache.shapeTemplateCount()); - } - - @Nonnull - private static WorldCollisionBuildStats worldCollisionStats( - @Nonnull WorldVoxelCollisionCache.BuildStats stats) { - return new WorldCollisionBuildStats(stats.scannedBlocks(), - stats.solidBlocks(), - stats.culledInteriorBlocks(), - stats.fullCubeRuns(), - stats.detailBoxes(), - stats.colliderBodies(), - stats.removedBodies(), - stats.sectionsBuilt(), - stats.sectionsRebuilt(), - stats.voxelBodies()); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/TerrainColliderMode.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/TerrainColliderMode.java deleted file mode 100644 index 83989d16..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/TerrainColliderMode.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision; - -/** - * Runtime representation used for full-cube terrain collision. - */ -public enum TerrainColliderMode { - MERGED_BOXES, - NATIVE_VOXELS_WHEN_SUPPORTED; - - public static TerrainColliderMode fromNativeVoxelTerrainEnabled(boolean enabled) { - return enabled ? NATIVE_VOXELS_WHEN_SUPPORTED : MERGED_BOXES; - } - - public boolean nativeVoxelTerrainEnabled() { - return this == NATIVE_VOXELS_WHEN_SUPPORTED; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldCollisionBuildOptions.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldCollisionBuildOptions.java deleted file mode 100644 index 402899bb..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldCollisionBuildOptions.java +++ /dev/null @@ -1,45 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision; - -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldCollisionSettings; -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Options that control generated world-collision backend geometry. - */ -public record WorldCollisionBuildOptions(@Nonnull TerrainColliderMode terrainColliderMode, - float terrainFriction, - float terrainRestitution) { - - public static final WorldCollisionBuildOptions DEFAULT = - fromNativeVoxelTerrainEnabled(PhysicsWorldCollisionSettings.DEFAULT_NATIVE_VOXEL_TERRAIN_ENABLED); - - public WorldCollisionBuildOptions { - Objects.requireNonNull(terrainColliderMode, "terrainColliderMode"); - if (!Float.isFinite(terrainFriction) || terrainFriction < 0.0f) { - throw new IllegalArgumentException("terrainFriction must be finite and >= 0"); - } - if (!Float.isFinite(terrainRestitution) || terrainRestitution < 0.0f) { - throw new IllegalArgumentException("terrainRestitution must be finite and >= 0"); - } - } - - @Nonnull - public static WorldCollisionBuildOptions fromSettings(@Nonnull PhysicsWorldCollisionSettings settings) { - return new WorldCollisionBuildOptions( - TerrainColliderMode.fromNativeVoxelTerrainEnabled(settings.isNativeVoxelTerrainEnabled()), - settings.getTerrainFriction(), - settings.getTerrainRestitution()); - } - - @Nonnull - public static WorldCollisionBuildOptions fromNativeVoxelTerrainEnabled(boolean enabled) { - return new WorldCollisionBuildOptions(TerrainColliderMode.fromNativeVoxelTerrainEnabled(enabled), - PhysicsWorldCollisionSettings.DEFAULT_TERRAIN_FRICTION, - PhysicsWorldCollisionSettings.DEFAULT_TERRAIN_RESTITUTION); - } - - public boolean nativeVoxelTerrainEnabled() { - return terrainColliderMode.nativeVoxelTerrainEnabled(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldCollisionLifecycle.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldCollisionLifecycle.java deleted file mode 100644 index cdeb770a..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldCollisionLifecycle.java +++ /dev/null @@ -1,59 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision; - -import dev.hytalemodding.impulse.core.internal.modules.SubPluginLifecycleGate; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Set; -import java.util.WeakHashMap; -import javax.annotation.Nonnull; - -/** - * Server-level lifecycle controlled by the Impulse world-collision subplugin. - */ -public final class WorldCollisionLifecycle { - - private static final SubPluginLifecycleGate GATE = - new SubPluginLifecycleGate("Impulse world collision subplugin is disabled"); - private static final Set RESOURCES = - Collections.newSetFromMap(new WeakHashMap<>()); - - static { - GATE.onDisable(WorldCollisionLifecycle::cleanupResources); - } - - private WorldCollisionLifecycle() { - } - - public static void enable() { - GATE.enable(); - } - - public static void disable() { - GATE.disable(); - } - - public static boolean isEnabled() { - return GATE.isEnabled(); - } - - public static long generation() { - return GATE.generation(); - } - - public static void registerResource(@Nonnull PhysicsWorldRuntimeResource resource) { - synchronized (RESOURCES) { - RESOURCES.add(resource); - } - } - - private static void cleanupResources() { - ArrayList resources; - synchronized (RESOURCES) { - resources = new ArrayList<>(RESOURCES); - } - for (PhysicsWorldRuntimeResource resource : resources) { - resource.disableWorldCollisionLifecycle(); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldVoxelCollisionCache.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldVoxelCollisionCache.java deleted file mode 100644 index c155776f..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldVoxelCollisionCache.java +++ /dev/null @@ -1,1491 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision; - -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.math.util.ChunkUtil; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.chunk.BlockChunk; -import com.hypixel.hytale.server.core.universe.world.chunk.section.BlockSection; -import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource.MissingSectionReason; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource.Snapshot; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource.StreamingTargetDiagnostic; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.SectionCollisionGeometry.BoxCollider; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.longs.Long2LongMap; -import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.longs.LongSet; -import it.unimi.dsi.fastutil.objects.Object2ObjectMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3d; - -/** - * Section-keyed cache that generates static physics collision from Hytale world blocks. - * - *

The cache is split per physics space so spaces can choose different world-collision - * policies. Each cached section is rebuilt when Hytale's section change counter changes, - * and removed when it falls out of the streaming radius or when its chunk unloads.

- */ -public final class WorldVoxelCollisionCache { - - private static final int ACTIVE_BODY_STREAMING_INTERVAL_TICKS = 4; - private static final int SLEEPING_BODY_STREAMING_INTERVAL_TICKS = 20; - private static final int MISSING_BLOCK_CHUNK_RETRY_TICKS = 10; - private static final int MISSING_BLOCK_SECTION_RETRY_TICKS = 5; - - private static final int ADJACENT_SECTION_VOXEL_SHIFT = 16; - private static final long BODY_TARGET_REFRESH_PENDING = Long.MIN_VALUE; - - private final Int2ObjectMap spaces = new Int2ObjectOpenHashMap<>(); - private final ShapeTemplateCache shapeTemplates = new ShapeTemplateCache(); - private final SectionColliderBuilder sectionBuilder = new SectionColliderBuilder(shapeTemplates); - private final AtomicBoolean streamingApplyPending = new AtomicBoolean(); - - public synchronized void copyFrom(@Nonnull WorldVoxelCollisionCache other) { - if (other == this) { - streamingApplyPending.set(false); - return; - } - spaces.clear(); - synchronized (other) { - for (Int2ObjectMap.Entry entry : other.spaces.int2ObjectEntrySet()) { - spaces.put(entry.getIntKey(), new SpaceCollisionCache(entry.getValue())); - } - } - streamingApplyPending.set(false); - } - - public boolean tryBeginStreamingApply() { - return streamingApplyPending.compareAndSet(false, true); - } - - public void finishStreamingApply() { - streamingApplyPending.set(false); - } - - public boolean isStreamingApplyPending() { - return streamingApplyPending.get(); - } - - @Nonnull - public SectionAccessCache newSectionAccessCache() { - return new SectionAccessCache(); - } - - /** - * Returns whether a body target needs terrain work. Call - * {@link #recordBodyTargetRefresh(SpaceId, RigidBodyKey, WorldCollisionStreamingBounds, boolean, long)} - * only after the terrain apply path has actually attempted that work. - */ - @Nonnull - public synchronized TargetRefreshDecision shouldRefreshBodyTarget(@Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyKey, - @Nonnull WorldCollisionStreamingBounds bounds, - boolean sleeping, - long currentTick, - int ttlTicks, - @Nullable Snapshot profiling) { - SpaceCollisionCache cache = spaces.computeIfAbsent(spaceId.value(), ignored -> new SpaceCollisionCache()); - CachedBodyStreamingTarget target = cache.bodyTargets.get(bodyKey); - if (target == null) { - cache.bodyTargets.put(bodyKey, new CachedBodyStreamingTarget(bounds, - sleeping, - currentTick, - BODY_TARGET_REFRESH_PENDING)); - if (profiling != null) { - profiling.incrementBodyTargetFirstSeen(); - } - return TargetRefreshDecision.refresh(TargetRefreshReason.FIRST_SEEN); - } - - target.lastSeenTick = currentTick; - target.sleeping = sleeping; - if (profiling != null) { - profiling.incrementBodyTargetCacheHits(); - } - - if (!target.bounds.equals(bounds)) { - target.bounds = bounds; - if (profiling != null) { - profiling.incrementBodyTargetBoundsChanged(); - } - return TargetRefreshDecision.refresh(TargetRefreshReason.BOUNDS_CHANGED); - } - - if (target.lastRefreshTick == BODY_TARGET_REFRESH_PENDING) { - return TargetRefreshDecision.refresh(TargetRefreshReason.PENDING_APPLY); - } - - int interval = sleeping ? sleepingBodyStreamingInterval(ttlTicks) : ACTIVE_BODY_STREAMING_INTERVAL_TICKS; - if (currentTick == 1L || currentTick - target.lastRefreshTick >= interval) { - if (profiling != null) { - if (sleeping) { - profiling.incrementBodyTargetSleepingRefreshes(); - } else { - profiling.incrementBodyTargetActiveRefreshes(); - } - } - return TargetRefreshDecision.refresh(sleeping - ? TargetRefreshReason.SLEEPING_INTERVAL - : TargetRefreshReason.ACTIVE_INTERVAL); - } - - if (profiling != null) { - if (sleeping) { - profiling.incrementBodyTargetSleepingStableSkips(); - } else { - profiling.incrementBodyTargetActiveStableSkips(); - } - } - return TargetRefreshDecision.skip(); - } - - /** - * Records that a body target's terrain refresh was attempted by the apply loop. - */ - public synchronized void recordBodyTargetRefresh(@Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyKey, - @Nonnull WorldCollisionStreamingBounds bounds, - boolean sleeping, - long currentTick) { - SpaceCollisionCache cache = spaces.computeIfAbsent(spaceId.value(), ignored -> new SpaceCollisionCache()); - CachedBodyStreamingTarget target = cache.bodyTargets.get(bodyKey); - if (target == null) { - cache.bodyTargets.put(bodyKey, new CachedBodyStreamingTarget(bounds, - sleeping, - currentTick, - currentTick)); - return; - } - target.bounds = bounds; - target.sleeping = sleeping; - target.lastSeenTick = currentTick; - target.lastRefreshTick = currentTick; - } - - public synchronized int pruneBodyStreamingTargets(@Nonnull SpaceId spaceId, - long currentTick, - int ttlTicks, - @Nullable Snapshot profiling) { - SpaceCollisionCache cache = spaces.get(spaceId.value()); - if (cache == null) { - return 0; - } - - long maxAge = Math.max(1L, ttlTicks) * 2L; - int removed = 0; - Iterator> iterator = - cache.bodyTargets.object2ObjectEntrySet().iterator(); - while (iterator.hasNext()) { - CachedBodyStreamingTarget target = iterator.next().getValue(); - if (currentTick - target.lastSeenTick <= maxAge) { - continue; - } - iterator.remove(); - removed++; - } - pruneExpiredMissingBackoffs(cache, currentTick); - if (cache.isEmpty()) { - spaces.remove(spaceId.value()); - } - if (removed > 0 && profiling != null) { - profiling.addBodyTargetsPruned(removed); - } - return removed; - } - - /** - * Wipes cached sections for the space, then rebuilds everything in the given radius. - */ - @Nonnull - public synchronized BuildStats rebuildAround(@Nonnull World world, - @Nonnull PhysicsSpaceBinding space, - @Nonnull Vector3d center, - int radius) { - return rebuildAround(world, - space, - center, - radius, - WorldCollisionBuildOptions.DEFAULT); - } - - /** - * Wipes cached sections for the space, then rebuilds everything in the given radius. - */ - @Nonnull - public synchronized BuildStats rebuildAround(@Nonnull World world, - @Nonnull PhysicsSpaceBinding space, - @Nonnull Vector3d center, - int radius, - @Nonnull WorldCollisionBuildOptions buildOptions) { - int removed = clear(space); - BuildStats stats = ensureAround(world, - space, - center, - radius, - 0L, - null, - null, - null, - null, - buildOptions); - return stats.withRemovedBodies(stats.removedBodies() + removed); - } - - /** - * Wipes cached sections in the given radius, then rebuilds that same radius. - */ - @Nonnull - public synchronized BuildStats refreshAround(@Nonnull World world, - @Nonnull PhysicsSpaceBinding space, - @Nonnull Vector3d center, - int radius, - @Nonnull WorldCollisionBuildOptions buildOptions) { - int removed = clearSectionsAround(space.spaceId(), space, center, radius); - BuildStats stats = ensureAround(world, - space, - center, - radius, - 0L, - null, - null, - null, - null, - buildOptions); - return stats.withRemovedBodies(stats.removedBodies() + removed); - } - - /** - * Ensures all chunk sections within the block radius around {@code center} are cached. - */ - @Nonnull - public synchronized BuildStats ensureAround(@Nonnull World world, - @Nonnull PhysicsSpaceBinding space, - @Nonnull Vector3d center, - int radius, - long tick) { - return ensureAround(world, space, center, radius, tick, null, null); - } - - /** - * Ensures all chunk sections within the block radius around {@code center} are cached. - */ - @Nonnull - public synchronized BuildStats ensureAround(@Nonnull World world, - @Nonnull PhysicsSpaceBinding space, - @Nonnull Vector3d center, - int radius, - long tick, - @Nullable Snapshot profiling) { - return ensureAround(world, space, center, radius, tick, profiling, null); - } - - /** - * Ensures all chunk sections within the block radius around {@code center} are cached. - */ - @Nonnull - public synchronized BuildStats ensureAround(@Nonnull World world, - @Nonnull PhysicsSpaceBinding space, - @Nonnull Vector3d center, - int radius, - long tick, - @Nullable Snapshot profiling, - @Nullable LongSet visitedSections) { - return ensureAround(world, space, center, radius, tick, profiling, visitedSections, null); - } - - /** - * Ensures all chunk sections within the block radius around {@code center} are cached. - */ - @Nonnull - public synchronized BuildStats ensureAround(@Nonnull World world, - @Nonnull PhysicsSpaceBinding space, - @Nonnull Vector3d center, - int radius, - long tick, - @Nullable Snapshot profiling, - @Nullable LongSet visitedSections, - @Nullable StreamingTargetDiagnostic targetDiagnostic) { - return ensureAround(world, - space, - center, - radius, - tick, - profiling, - visitedSections, - targetDiagnostic, - null); - } - - /** - * Ensures all chunk sections within the block radius around {@code center} are cached. - */ - @Nonnull - public synchronized BuildStats ensureAround(@Nonnull World world, - @Nonnull PhysicsSpaceBinding space, - @Nonnull Vector3d center, - int radius, - long tick, - @Nullable Snapshot profiling, - @Nullable LongSet visitedSections, - @Nullable StreamingTargetDiagnostic targetDiagnostic, - @Nullable SectionAccessCache accessCache) { - return ensureAround(world, - space, - center, - radius, - tick, - profiling, - visitedSections, - targetDiagnostic, - accessCache, - WorldCollisionBuildOptions.DEFAULT); - } - - /** - * Ensures all chunk sections within the block radius around {@code center} are cached. - */ - @Nonnull - public synchronized BuildStats ensureAround(@Nonnull World world, - @Nonnull PhysicsSpaceBinding space, - @Nonnull Vector3d center, - int radius, - long tick, - @Nullable Snapshot profiling, - @Nullable LongSet visitedSections, - @Nullable StreamingTargetDiagnostic targetDiagnostic, - @Nullable SectionAccessCache accessCache, - @Nonnull WorldCollisionBuildOptions buildOptions) { - long start = profiling != null ? System.nanoTime() : 0L; - if (profiling != null) { - profiling.incrementEnsureCalls(); - } - - int minX = (int) Math.floor(center.x) - radius; - int maxX = (int) Math.floor(center.x) + radius; - int minY = Math.max(0, (int) Math.floor(center.y) - radius); - int maxY = Math.min(ChunkUtil.HEIGHT_MINUS_1, (int) Math.floor(center.y) + radius); - int minZ = (int) Math.floor(center.z) - radius; - int maxZ = (int) Math.floor(center.z) + radius; - - int minChunkX = ChunkUtil.chunkCoordinate(minX); - int maxChunkX = ChunkUtil.chunkCoordinate(maxX); - int minSectionY = ChunkUtil.indexSection(minY); - int maxSectionY = ChunkUtil.indexSection(maxY); - int minChunkZ = ChunkUtil.chunkCoordinate(minZ); - int maxChunkZ = ChunkUtil.chunkCoordinate(maxZ); - - BuildStats total = BuildStats.empty(); - for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { - for (int sectionY = minSectionY; sectionY <= maxSectionY; sectionY++) { - for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { - long key = packSectionKey(chunkX, sectionY, chunkZ); - if (visitedSections != null) { - if (!visitedSections.add(key)) { - if (profiling != null) { - profiling.incrementDuplicateSkips(); - } - continue; - } - } - total = total.plus(ensureSection( - world, - space, - chunkX, - sectionY, - chunkZ, - tick, - profiling, - targetDiagnostic, - accessCache, - buildOptions)); - } - } - } - if (profiling != null) { - profiling.addEnsureAroundNanos(System.nanoTime() - start); - } - return total; - } - - /** - * Refreshes the TTL of already-cached sections around a sleeping body without building - * missing collision or touching chunk storage. - */ - public synchronized int touchAround(@Nonnull SpaceId spaceId, - @Nonnull Vector3d center, - int radius, - long tick) { - SpaceCollisionCache cache = spaces.get(spaceId.value()); - if (cache == null) { - return 0; - } - - int minX = (int) Math.floor(center.x) - radius; - int maxX = (int) Math.floor(center.x) + radius; - int minY = Math.max(0, (int) Math.floor(center.y) - radius); - int maxY = Math.min(ChunkUtil.HEIGHT_MINUS_1, (int) Math.floor(center.y) + radius); - int minZ = (int) Math.floor(center.z) - radius; - int maxZ = (int) Math.floor(center.z) + radius; - - int minChunkX = ChunkUtil.chunkCoordinate(minX); - int maxChunkX = ChunkUtil.chunkCoordinate(maxX); - int minSectionY = ChunkUtil.indexSection(minY); - int maxSectionY = ChunkUtil.indexSection(maxY); - int minChunkZ = ChunkUtil.chunkCoordinate(minZ); - int maxChunkZ = ChunkUtil.chunkCoordinate(maxZ); - - int touched = 0; - for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { - for (int sectionY = minSectionY; sectionY <= maxSectionY; sectionY++) { - for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { - CachedSection section = cache.section(chunkX, sectionY, chunkZ); - if (section == null || section.lastUsedTick >= tick) { - continue; - } - section.lastUsedTick = tick; - touched++; - } - } - } - return touched; - } - - /** - * Removes sections whose last use is older than the configured TTL. - */ - public synchronized int pruneUnused(@Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceBinding space, - long currentTick, - int ttlTicks) { - return pruneUnused(spaceId, space, currentTick, ttlTicks, null); - } - - /** - * Removes sections whose last use is older than the configured TTL. - */ - public synchronized int pruneUnused(@Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceBinding space, - long currentTick, - int ttlTicks, - @Nullable Snapshot profiling) { - long start = profiling != null ? System.nanoTime() : 0L; - SpaceCollisionCache cache = spaces.get(spaceId.value()); - if (cache == null) { - return 0; - } - - int removed = 0; - int removedSections = 0; - Iterator> iterator = cache.sections.long2ObjectEntrySet().iterator(); - while (iterator.hasNext()) { - Long2ObjectMap.Entry entry = iterator.next(); - CachedSection section = entry.getValue(); - if (currentTick - section.lastUsedTick <= ttlTicks) { - continue; - } - - removed += section.removeFrom(space); - removedSections++; - iterator.remove(); - } - pruneExpiredMissingBackoffs(cache, currentTick); - if (cache.isEmpty()) { - spaces.remove(spaceId.value()); - } - if (profiling != null) { - profiling.addTtlPrune(removedSections, removed); - profiling.addPruneUnusedNanos(System.nanoTime() - start); - } - return removed; - } - - /** - * Removes cached sections whose source chunk is no longer loaded. - */ - public synchronized int pruneUnloaded(@Nonnull World world, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceBinding space) { - return pruneUnloaded(world, spaceId, space, null); - } - - /** - * Removes cached sections whose source chunk is no longer loaded. - */ - public synchronized int pruneUnloaded(@Nonnull World world, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceBinding space, - @Nullable Snapshot profiling) { - return pruneUnloaded(world, spaceId, space, profiling, null); - } - - /** - * Removes cached sections whose source chunk is no longer loaded. - */ - public synchronized int pruneUnloaded(@Nonnull World world, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceBinding space, - @Nullable Snapshot profiling, - @Nullable SectionAccessCache accessCache) { - long start = profiling != null ? System.nanoTime() : 0L; - SpaceCollisionCache cache = spaces.get(spaceId.value()); - if (cache == null) { - return 0; - } - - int removed = 0; - int removedSections = 0; - Iterator> iterator = cache.sections.long2ObjectEntrySet().iterator(); - while (iterator.hasNext()) { - CachedSection section = iterator.next().getValue(); - if (blockChunk(world, section.chunkX, section.chunkZ, accessCache) != null) { - continue; - } - - removed += section.removeFrom(space); - removedSections++; - iterator.remove(); - } - if (cache.isEmpty()) { - spaces.remove(spaceId.value()); - } - if (profiling != null) { - profiling.addUnloadedPrune(removedSections, removed); - profiling.addPruneUnloadedNanos(System.nanoTime() - start); - } - return removed; - } - - /** - * Removes all cached sections for the given space. - */ - public synchronized int clear(@Nonnull SpaceId spaceId, @Nullable PhysicsSpaceBinding space) { - SpaceCollisionCache cache = spaces.remove(spaceId.value()); - if (cache == null || space == null) { - return 0; - } - - int removed = 0; - for (CachedSection section : cache.sections.values()) { - removed += section.removeFrom(space); - } - return removed; - } - - /** - * Removes all cached sections for the given space. - */ - public synchronized int clear(@Nonnull PhysicsSpaceBinding space) { - return clear(space.spaceId(), space); - } - - /** - * Removes cached sections within the block radius around {@code center}. - */ - public synchronized int clearSectionsAround(@Nonnull SpaceId spaceId, - @Nullable PhysicsSpaceBinding space, - @Nonnull Vector3d center, - int radius) { - SpaceCollisionCache cache = spaces.get(spaceId.value()); - if (cache == null) { - return 0; - } - - int clampedRadius = Math.max(0, radius); - int minX = (int) Math.floor(center.x) - clampedRadius; - int maxX = (int) Math.floor(center.x) + clampedRadius; - int minY = Math.max(0, (int) Math.floor(center.y) - clampedRadius); - int maxY = Math.min(ChunkUtil.HEIGHT_MINUS_1, (int) Math.floor(center.y) + clampedRadius); - int minZ = (int) Math.floor(center.z) - clampedRadius; - int maxZ = (int) Math.floor(center.z) + clampedRadius; - - int minChunkX = ChunkUtil.chunkCoordinate(minX); - int maxChunkX = ChunkUtil.chunkCoordinate(maxX); - int minSectionY = ChunkUtil.indexSection(minY); - int maxSectionY = ChunkUtil.indexSection(maxY); - int minChunkZ = ChunkUtil.chunkCoordinate(minZ); - int maxChunkZ = ChunkUtil.chunkCoordinate(maxZ); - - int removed = 0; - Iterator> iterator = cache.sections.long2ObjectEntrySet().iterator(); - while (iterator.hasNext()) { - CachedSection section = iterator.next().getValue(); - if (section.chunkX < minChunkX || section.chunkX > maxChunkX - || section.sectionY < minSectionY || section.sectionY > maxSectionY - || section.chunkZ < minChunkZ || section.chunkZ > maxChunkZ) { - continue; - } - if (space != null) { - removed += section.removeFrom(space); - } - iterator.remove(); - } - for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { - for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { - cache.missingBlockChunkBackoffs.remove(ChunkUtil.indexChunk(chunkX, chunkZ)); - removeSectionBackoffsForChunk(cache, chunkX, chunkZ); - } - } - if (cache.isEmpty()) { - spaces.remove(spaceId.value()); - } - return removed; - } - - /** - * Removes cached sections for one chunk from a single physics space. - */ - public synchronized int clearChunk(@Nonnull SpaceId spaceId, - @Nullable PhysicsSpaceBinding space, - int chunkX, - int chunkZ) { - SpaceCollisionCache cache = spaces.get(spaceId.value()); - if (cache == null) { - return 0; - } - - int removed = 0; - Iterator> iterator = cache.sections.long2ObjectEntrySet().iterator(); - while (iterator.hasNext()) { - CachedSection section = iterator.next().getValue(); - if (section.chunkX != chunkX || section.chunkZ != chunkZ) { - continue; - } - if (space != null) { - removed += section.removeFrom(space); - } - iterator.remove(); - } - cache.missingBlockChunkBackoffs.remove(ChunkUtil.indexChunk(chunkX, chunkZ)); - removeSectionBackoffsForChunk(cache, chunkX, chunkZ); - if (cache.isEmpty()) { - spaces.remove(spaceId.value()); - } - return removed; - } - - public synchronized int bodyCount() { - int count = 0; - for (SpaceCollisionCache cache : spaces.values()) { - for (CachedSection section : cache.sections.values()) { - count += section.backendBodyIds.size(); - } - } - return count; - } - - public synchronized int bodyCount(@Nonnull SpaceId spaceId) { - SpaceCollisionCache cache = spaces.get(spaceId.value()); - if (cache == null) { - return 0; - } - - int count = 0; - for (CachedSection section : cache.sections.values()) { - count += section.backendBodyIds.size(); - } - return count; - } - - public synchronized int sectionCount() { - int count = 0; - for (SpaceCollisionCache cache : spaces.values()) { - count += cache.sections.size(); - } - return count; - } - - public synchronized int spaceCount() { - return spaces.size(); - } - - public synchronized int shapeTemplateCount() { - return shapeTemplates.size(); - } - - public synchronized boolean containsBody(@Nonnull SpaceId spaceId, long backendBodyId) { - SpaceCollisionCache cache = spaces.get(spaceId.value()); - if (cache == null) { - return false; - } - - for (CachedSection section : cache.sections.values()) { - if (section.backendBodyIds.contains(backendBodyId)) { - return true; - } - } - return false; - } - - /** - * Probes the highest cached world-collision surface under a body footprint. - *

- * This is intended for diagnostics, not simulation. It uses the collision geometry - * already built for a physics space so benchmark health checks can compare bodies to - * the streamed terrain actually present in the backend instead of a fixed Y plane.

- */ - @Nonnull - public synchronized GroundProbe probeGround(@Nonnull SpaceId spaceId, - double x, - double z, - double horizontalHalfExtent) { - SpaceCollisionCache cache = spaces.get(spaceId.value()); - if (cache == null) { - return GroundProbe.missing(); - } - - double halfExtent = Math.max(0.0, horizontalHalfExtent); - double minX = x - halfExtent; - double maxX = x + halfExtent; - double minZ = z - halfExtent; - double maxZ = z + halfExtent; - int minChunkX = ChunkUtil.chunkCoordinate((int) Math.floor(minX)); - int maxChunkX = ChunkUtil.chunkCoordinate((int) Math.floor(maxX)); - int minChunkZ = ChunkUtil.chunkCoordinate((int) Math.floor(minZ)); - int maxChunkZ = ChunkUtil.chunkCoordinate((int) Math.floor(maxZ)); - - double groundTopY = Double.NEGATIVE_INFINITY; - for (CachedSection section : cache.sections.values()) { - if (section.chunkX < minChunkX || section.chunkX > maxChunkX - || section.chunkZ < minChunkZ || section.chunkZ > maxChunkZ) { - continue; - } - groundTopY = probeGroundTop(section.fullCubeBoxes, minX, maxX, minZ, maxZ, groundTopY); - groundTopY = probeGroundTop(section.detailBoxes, minX, maxX, minZ, maxZ, groundTopY); - } - if (groundTopY == Double.NEGATIVE_INFINITY) { - return GroundProbe.missing(); - } - return new GroundProbe(true, groundTopY); - } - - private static double probeGroundTop(@Nonnull List boxes, - double minX, - double maxX, - double minZ, - double maxZ, - double groundTopY) { - double topY = groundTopY; - for (BoxCollider box : boxes) { - if (!overlaps(minX, maxX, box.centerX() - box.halfX(), box.centerX() + box.halfX()) - || !overlaps(minZ, maxZ, box.centerZ() - box.halfZ(), box.centerZ() + box.halfZ())) { - continue; - } - topY = Math.max(topY, box.centerY() + box.halfY()); - } - return topY; - } - - private static boolean overlaps(double minA, double maxA, double minB, double maxB) { - return maxA >= minB && maxB >= minA; - } - - public synchronized void forEachDebugSection(@Nonnull SpaceId spaceId, - @Nonnull Consumer consumer) { - SpaceCollisionCache cache = spaces.get(spaceId.value()); - if (cache == null) { - return; - } - - for (CachedSection section : cache.sections.values()) { - consumer.accept(section.debugSection()); - } - } - - @Nonnull - private BuildStats ensureSection(@Nonnull World world, - @Nonnull PhysicsSpaceBinding space, - int chunkX, - int sectionY, - int chunkZ, - long tick, - @Nullable Snapshot profiling, - @Nullable StreamingTargetDiagnostic targetDiagnostic, - @Nullable SectionAccessCache accessCache, - @Nonnull WorldCollisionBuildOptions buildOptions) { - long start = profiling != null ? System.nanoTime() : 0L; - if (profiling != null) { - profiling.incrementSectionRequests(); - } - - SpaceCollisionCache cache = - spaces.computeIfAbsent(space.spaceId().value(), ignored -> new SpaceCollisionCache()); - long chunkKey = ChunkUtil.indexChunk(chunkX, chunkZ); - long sectionKey = packSectionKey(chunkX, sectionY, chunkZ); - if (isBackedOff(cache.missingBlockChunkBackoffs, chunkKey, tick)) { - if (profiling != null) { - profiling.incrementMissingBackoffSkip(MissingSectionReason.BLOCK_CHUNK); - profiling.recordMissingSection(MissingSectionReason.BLOCK_CHUNK, - chunkX, - sectionY, - chunkZ, - targetDiagnostic); - profiling.addEnsureSectionNanos(System.nanoTime() - start); - } - return BuildStats.empty(); - } - - BlockChunk blockChunk = blockChunk(world, chunkX, chunkZ, accessCache); - if (blockChunk == null) { - cache.missingBlockChunkBackoffs.put(chunkKey, tick + MISSING_BLOCK_CHUNK_RETRY_TICKS); - if (profiling != null) { - profiling.recordMissingSection(MissingSectionReason.BLOCK_CHUNK, - chunkX, - sectionY, - chunkZ, - targetDiagnostic); - profiling.addEnsureSectionNanos(System.nanoTime() - start); - } - return BuildStats.empty(); - } - cache.missingBlockChunkBackoffs.remove(chunkKey); - - if (isBackedOff(cache.missingBlockSectionBackoffs, sectionKey, tick)) { - if (profiling != null) { - profiling.incrementMissingBackoffSkip(MissingSectionReason.BLOCK_SECTION); - profiling.recordMissingSection(MissingSectionReason.BLOCK_SECTION, - chunkX, - sectionY, - chunkZ, - targetDiagnostic); - profiling.addEnsureSectionNanos(System.nanoTime() - start); - } - return BuildStats.empty(); - } - - BlockSection section = accessCache != null - ? accessCache.blockSection(world, chunkX, sectionY, chunkZ) - : ChunkSectionAccess.blockSection(world, chunkX, sectionY, chunkZ); - if (section == null) { - cache.missingBlockSectionBackoffs.put(sectionKey, tick + MISSING_BLOCK_SECTION_RETRY_TICKS); - if (profiling != null) { - profiling.recordMissingSection(MissingSectionReason.BLOCK_SECTION, - chunkX, - sectionY, - chunkZ, - targetDiagnostic); - profiling.addEnsureSectionNanos(System.nanoTime() - start); - } - return BuildStats.empty(); - } - cache.missingBlockSectionBackoffs.remove(sectionKey); - CachedSection cached = cache.sections.get(sectionKey); - long neighborhoodSignature = sectionBuilder.neighborhoodSignature(world, - section, - chunkX, - sectionY, - chunkZ, - accessCache); - if (cached != null - && cached.neighborhoodSignature == neighborhoodSignature - && cached.buildOptions.equals(buildOptions)) { - cached.lastUsedTick = tick; - if (profiling != null) { - profiling.incrementSectionCacheHits(); - profiling.addEnsureSectionNanos(System.nanoTime() - start); - } - return BuildStats.empty(); - } - - SectionCollisionGeometry geometry = sectionBuilder.build(world, - section, - chunkX, - sectionY, - chunkZ, - accessCache); - CachedSection built = new CachedSection(chunkX, sectionY, chunkZ, tick, - neighborhoodSignature); - built.buildOptions = buildOptions; - try { - addGeometryBodies(space, - built, - geometry, - chunkX, - sectionY, - chunkZ, - buildOptions); - } catch (RuntimeException exception) { - removeBuiltSectionAfterFailure(space, built, exception); - throw exception; - } - - int removed = 0; - boolean rebuilt = cached != null; - try { - if (cached != null) { - removed = cached.removeFrom(space); - } - } catch (RuntimeException exception) { - removeBuiltSectionAfterFailure(space, built, exception); - throw exception; - } - cache.sections.put(sectionKey, built); - try { - stitchAdjacentVoxelTerrains(space, cache, built); - } catch (RuntimeException exception) { - cache.sections.remove(sectionKey); - removeBuiltSectionAfterFailure(space, built, exception); - throw exception; - } - - BuildStats stats = BuildStats.from(geometry, - built.backendBodyIds.size(), - removed, - rebuilt ? 0 : 1, - rebuilt ? 1 : 0, - built.voxelTerrain ? 1 : 0); - if (profiling != null) { - profiling.addBuildStats(stats); - profiling.addEnsureSectionNanos(System.nanoTime() - start); - } - return stats; - } - - private static void removeBuiltSectionAfterFailure(@Nonnull PhysicsSpaceBinding space, - @Nonnull CachedSection built, - @Nonnull RuntimeException failure) { - try { - built.removeFrom(space); - } catch (RuntimeException cleanupFailure) { - failure.addSuppressed(cleanupFailure); - } - } - - private static void addGeometryBodies(@Nonnull PhysicsSpaceBinding space, - @Nonnull CachedSection target, - @Nonnull SectionCollisionGeometry geometry, - int chunkX, - int sectionY, - int chunkZ, - @Nonnull WorldCollisionBuildOptions buildOptions) { - target.fullCubeBoxes.addAll(geometry.mergedFullCubeBoxes()); - target.detailBoxes.addAll(geometry.detailBoxes()); - if (buildOptions.nativeVoxelTerrainEnabled() - && geometry.hasFullCubeVoxels() - && space.runtime().supportsVoxelTerrain(space.backendSpaceHandle().value())) { - addVoxelTerrain(space, target, geometry, chunkX, sectionY, chunkZ, buildOptions); - } else { - for (BoxCollider box : geometry.mergedFullCubeBoxes()) { - addStaticBox(space, target, box, buildOptions); - } - } - - for (BoxCollider box : geometry.detailBoxes()) { - addStaticBox(space, target, box, buildOptions); - } - } - - private static void addVoxelTerrain(@Nonnull PhysicsSpaceBinding space, - @Nonnull CachedSection section, - @Nonnull SectionCollisionGeometry geometry, - int chunkX, - int sectionY, - int chunkZ, - @Nonnull WorldCollisionBuildOptions buildOptions) { - long backendBodyId = space.runtime().createVoxelTerrain(space.backendSpaceHandle().value(), - 1.0f, - 1.0f, - 1.0f, - geometry.fullCubeVoxels(), - chunkX << ChunkUtil.BITS, - sectionY << ChunkUtil.BITS, - chunkZ << ChunkUtil.BITS, - buildOptions.terrainFriction(), - buildOptions.terrainRestitution(), - PhysicsCollisionFilters.TERRAIN, - PhysicsCollisionFilters.ALL); - section.backendBodyIds.add(backendBodyId); - section.voxelTerrainBodyId = backendBodyId; - section.voxelTerrain = true; - } - - private static void addStaticBox(@Nonnull PhysicsSpaceBinding space, - @Nonnull CachedSection section, - @Nonnull BoxCollider box, - @Nonnull WorldCollisionBuildOptions buildOptions) { - if (box.halfX() <= 0.0 || box.halfY() <= 0.0 || box.halfZ() <= 0.0) { - return; - } - - long backendBodyId = space.runtime().createBody(space.backendSpaceHandle().value(), - BackendRuntimeCodes.SHAPE_BOX, - (float) box.halfX(), - (float) box.halfY(), - (float) box.halfZ(), - 0.0f, - 0.0f, - BackendRuntimeCodes.AXIS_Y, - 0.0f, - 0.0f, - BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.STATIC), - (float) box.centerX(), - (float) box.centerY(), - (float) box.centerZ(), - 0.0f, - 0.0f, - 0.0f, - 1.0f); - section.backendBodyIds.add(backendBodyId); - applyTerrainMaterial(space, backendBodyId, buildOptions); - space.runtime() - .setBodyCollisionFilter(space.backendSpaceHandle().value(), - backendBodyId, - PhysicsCollisionFilters.TERRAIN, - PhysicsCollisionFilters.ALL); - } - - private static void applyTerrainMaterial(@Nonnull PhysicsSpaceBinding space, - long backendBodyId, - @Nonnull WorldCollisionBuildOptions buildOptions) { - // TODO: Replace coarse terrain settings with real per-block material lookup. - space.runtime().setBodyFriction(space.backendSpaceHandle().value(), - backendBodyId, - buildOptions.terrainFriction()); - space.runtime().setBodyRestitution(space.backendSpaceHandle().value(), - backendBodyId, - buildOptions.terrainRestitution()); - } - - private static void stitchAdjacentVoxelTerrains(@Nonnull PhysicsSpaceBinding space, - @Nonnull SpaceCollisionCache cache, - @Nonnull CachedSection built) { - if (!built.hasVoxelTerrainBody()) { - return; - } - - stitchVoxelTerrain(space, - built, - cache.section(built.chunkX - 1, built.sectionY, built.chunkZ), - -ADJACENT_SECTION_VOXEL_SHIFT, - 0, - 0); - stitchVoxelTerrain(space, - built, - cache.section(built.chunkX + 1, built.sectionY, built.chunkZ), - ADJACENT_SECTION_VOXEL_SHIFT, - 0, - 0); - stitchVoxelTerrain(space, - built, - cache.section(built.chunkX, built.sectionY - 1, built.chunkZ), - 0, - -ADJACENT_SECTION_VOXEL_SHIFT, - 0); - stitchVoxelTerrain(space, - built, - cache.section(built.chunkX, built.sectionY + 1, built.chunkZ), - 0, - ADJACENT_SECTION_VOXEL_SHIFT, - 0); - stitchVoxelTerrain(space, - built, - cache.section(built.chunkX, built.sectionY, built.chunkZ - 1), - 0, - 0, - -ADJACENT_SECTION_VOXEL_SHIFT); - stitchVoxelTerrain(space, - built, - cache.section(built.chunkX, built.sectionY, built.chunkZ + 1), - 0, - 0, - ADJACENT_SECTION_VOXEL_SHIFT); - } - - private static void stitchVoxelTerrain(@Nonnull PhysicsSpaceBinding space, - @Nonnull CachedSection built, - @Nullable CachedSection neighbor, - int shiftX, - int shiftY, - int shiftZ) { - if (neighbor == null || !neighbor.hasVoxelTerrainBody()) { - return; - } - space.runtime().combineVoxelTerrains(space.backendSpaceHandle().value(), - built.voxelTerrainBodyId, - neighbor.voxelTerrainBodyId, - shiftX, - shiftY, - shiftZ); - } - - @Nullable - private static BlockChunk blockChunk(@Nonnull World world, int chunkX, int chunkZ) { - return blockChunk(world, chunkX, chunkZ, null); - } - - @Nullable - private static BlockChunk blockChunk(@Nonnull World world, - int chunkX, - int chunkZ, - @Nullable SectionAccessCache accessCache) { - return accessCache != null - ? accessCache.blockChunk(world, chunkX, chunkZ) - : loadBlockChunk(world, chunkX, chunkZ); - } - - @Nullable - private static BlockChunk loadBlockChunk(@Nonnull World world, int chunkX, int chunkZ) { - Ref chunkRef = world.getChunkStore() - .getChunkReference(ChunkUtil.indexChunk(chunkX, chunkZ)); - if (chunkRef == null || !chunkRef.isValid()) { - return null; - } - Store store = world.getChunkStore().getStore(); - return store.getComponentConcurrent(chunkRef, BlockChunk.getComponentType()); - } - - private static int sleepingBodyStreamingInterval(int ttlTicks) { - return Math.clamp(ttlTicks / 2, 1, SLEEPING_BODY_STREAMING_INTERVAL_TICKS); - } - - private static boolean isBackedOff(@Nonnull Long2LongMap backoffs, long key, long currentTick) { - if (!backoffs.containsKey(key)) { - return false; - } - - long retryTick = backoffs.get(key); - if (currentTick < retryTick) { - return true; - } - - backoffs.remove(key); - return false; - } - - private static void pruneExpiredMissingBackoffs(@Nonnull SpaceCollisionCache cache, long currentTick) { - pruneExpiredMissingBackoffs(cache.missingBlockChunkBackoffs, currentTick); - pruneExpiredMissingBackoffs(cache.missingBlockSectionBackoffs, currentTick); - } - - private static void pruneExpiredMissingBackoffs(@Nonnull Long2LongMap backoffs, long currentTick) { - backoffs.long2LongEntrySet().removeIf(entry -> currentTick >= entry.getLongValue()); - } - - private static void removeSectionBackoffsForChunk(@Nonnull SpaceCollisionCache cache, - int chunkX, - int chunkZ) { - Iterator iterator = cache.missingBlockSectionBackoffs.long2LongEntrySet().iterator(); - while (iterator.hasNext()) { - long key = iterator.next().getLongKey(); - if (unpackChunkX(key) == chunkX && unpackChunkZ(key) == chunkZ) { - iterator.remove(); - } - } - } - - private static long packSectionKey(int chunkX, int sectionY, int chunkZ) { - return ((long) chunkX & 0x3FF_FFFFL) << 38 - | ((long) chunkZ & 0x3FF_FFFFL) << 12 - | (sectionY & 0xFFFL); - } - - private static int unpackChunkX(long sectionKey) { - return signExtend26((int) (sectionKey >>> 38)); - } - - private static int unpackChunkZ(long sectionKey) { - return signExtend26((int) ((sectionKey >>> 12) & 0x3FF_FFFFL)); - } - - private static int signExtend26(int value) { - int signBit = 1 << 25; - return (value ^ signBit) - signBit; - } - - /** - * Per-space cache so pruning and clearing do not scan unrelated spaces. - */ - private static final class SpaceCollisionCache { - - private final Long2ObjectMap sections = new Long2ObjectOpenHashMap<>(); - private final Object2ObjectMap bodyTargets = - new Object2ObjectOpenHashMap<>(); - private final Long2LongMap missingBlockChunkBackoffs = new Long2LongOpenHashMap(); - private final Long2LongMap missingBlockSectionBackoffs = new Long2LongOpenHashMap(); - - private SpaceCollisionCache() { - } - - private SpaceCollisionCache(@Nonnull SpaceCollisionCache other) { - for (Long2ObjectMap.Entry entry : other.sections.long2ObjectEntrySet()) { - sections.put(entry.getLongKey(), new CachedSection(entry.getValue())); - } - for (Object2ObjectMap.Entry entry - : other.bodyTargets.object2ObjectEntrySet()) { - bodyTargets.put(entry.getKey(), new CachedBodyStreamingTarget(entry.getValue())); - } - missingBlockChunkBackoffs.putAll(other.missingBlockChunkBackoffs); - missingBlockSectionBackoffs.putAll(other.missingBlockSectionBackoffs); - } - - @Nullable - private CachedSection section(int chunkX, int sectionY, int chunkZ) { - return sections.get(packSectionKey(chunkX, sectionY, chunkZ)); - } - - private boolean isEmpty() { - return sections.isEmpty() - && bodyTargets.isEmpty() - && missingBlockChunkBackoffs.isEmpty() - && missingBlockSectionBackoffs.isEmpty(); - } - } - - private static final class CachedBodyStreamingTarget { - - private WorldCollisionStreamingBounds bounds; - private boolean sleeping; - private long lastSeenTick; - private long lastRefreshTick; - - private CachedBodyStreamingTarget(@Nonnull WorldCollisionStreamingBounds bounds, - boolean sleeping, - long lastSeenTick, - long lastRefreshTick) { - this.bounds = bounds; - this.sleeping = sleeping; - this.lastSeenTick = lastSeenTick; - this.lastRefreshTick = lastRefreshTick; - } - - private CachedBodyStreamingTarget(@Nonnull CachedBodyStreamingTarget other) { - this(other.bounds, other.sleeping, other.lastSeenTick, other.lastRefreshTick); - } - } - - /** - * Generated static backend bodies for one chunk section. - */ - private static final class CachedSection { - - private final int chunkX; - private final int sectionY; - private final int chunkZ; - private final List backendBodyIds = new ArrayList<>(); - private final List fullCubeBoxes = new ArrayList<>(); - private final List detailBoxes = new ArrayList<>(); - private final long neighborhoodSignature; - @Nonnull - private WorldCollisionBuildOptions buildOptions = WorldCollisionBuildOptions.DEFAULT; - private boolean voxelTerrain; - private long voxelTerrainBodyId; - private long lastUsedTick; - - private CachedSection(int chunkX, - int sectionY, - int chunkZ, - long lastUsedTick, - long neighborhoodSignature) { - this.chunkX = chunkX; - this.sectionY = sectionY; - this.chunkZ = chunkZ; - this.lastUsedTick = lastUsedTick; - this.neighborhoodSignature = neighborhoodSignature; - } - - private CachedSection(@Nonnull CachedSection other) { - this(other.chunkX, - other.sectionY, - other.chunkZ, - other.lastUsedTick, - other.neighborhoodSignature); - backendBodyIds.addAll(other.backendBodyIds); - fullCubeBoxes.addAll(other.fullCubeBoxes); - detailBoxes.addAll(other.detailBoxes); - buildOptions = other.buildOptions; - voxelTerrain = other.voxelTerrain; - voxelTerrainBodyId = other.voxelTerrainBodyId; - } - - private int removeFrom(@Nonnull PhysicsSpaceBinding space) { - int removed = backendBodyIds.size(); - for (long backendBodyId : backendBodyIds) { - space.runtime().removeBody(space.backendSpaceHandle().value(), backendBodyId); - } - backendBodyIds.clear(); - voxelTerrainBodyId = 0L; - voxelTerrain = false; - return removed; - } - - private boolean hasVoxelTerrainBody() { - return voxelTerrain && voxelTerrainBodyId != 0L; - } - - @Nonnull - private DebugSection debugSection() { - return new DebugSection(chunkX, - sectionY, - chunkZ, - backendBodyIds.size(), - voxelTerrain, - List.copyOf(fullCubeBoxes), - List.copyOf(detailBoxes)); - } - } - - /** - * Immutable debug snapshot for one cached world-collision section. - */ - public record DebugSection(int chunkX, - int sectionY, - int chunkZ, - int bodyCount, - boolean voxelTerrain, - @Nonnull List fullCubeBoxes, - @Nonnull List detailBoxes) { - } - - /** - * Highest cached terrain surface found under a queried body footprint. - */ - public record GroundProbe(boolean found, double topY) { - - @Nonnull - private static GroundProbe missing() { - return new GroundProbe(false, Double.NaN); - } - } - - /** - * Per-streaming-tick source-section cache. It avoids repeated Hytale chunk - * reference lookups while many bodies request overlapping neighborhoods. - */ - public static final class SectionAccessCache { - - private final Long2ObjectMap blockChunks = new Long2ObjectOpenHashMap<>(); - private final Long2ObjectMap blockSections = new Long2ObjectOpenHashMap<>(); - - private SectionAccessCache() { - } - - @Nullable - BlockChunk blockChunk(@Nonnull World world, int chunkX, int chunkZ) { - long key = ChunkUtil.indexChunk(chunkX, chunkZ); - if (blockChunks.containsKey(key)) { - return blockChunks.get(key); - } - - BlockChunk chunk = loadBlockChunk(world, chunkX, chunkZ); - blockChunks.put(key, chunk); - return chunk; - } - - @Nullable - BlockSection blockSection(@Nonnull World world, int chunkX, int sectionY, int chunkZ) { - long key = packSectionKey(chunkX, sectionY, chunkZ); - if (blockSections.containsKey(key)) { - return blockSections.get(key); - } - - BlockSection section = ChunkSectionAccess.blockSection(world, chunkX, sectionY, chunkZ); - blockSections.put(key, section); - return section; - } - } - - public enum TargetRefreshReason { - FIRST_SEEN, - BOUNDS_CHANGED, - PENDING_APPLY, - ACTIVE_INTERVAL, - SLEEPING_INTERVAL, - STABLE_SKIP - } - - public record TargetRefreshDecision(boolean refresh, @Nonnull TargetRefreshReason reason) { - - @Nonnull - private static TargetRefreshDecision refresh(@Nonnull TargetRefreshReason reason) { - return new TargetRefreshDecision(true, reason); - } - - @Nonnull - private static TargetRefreshDecision skip() { - return new TargetRefreshDecision(false, TargetRefreshReason.STABLE_SKIP); - } - } - - /** - * Aggregate statistics from a cache build or rebuild operation. - */ - public record BuildStats(int scannedBlocks, - int solidBlocks, - int culledInteriorBlocks, - int fullCubeRuns, - int detailBoxes, - int colliderBodies, - int removedBodies, - int sectionsBuilt, - int sectionsRebuilt, - int voxelBodies) { - - @Nonnull - public static BuildStats empty() { - return new BuildStats(0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - } - - @Nonnull - private static BuildStats from(@Nonnull SectionCollisionGeometry geometry, - int colliderBodies, - int removedBodies, - int sectionsBuilt, - int sectionsRebuilt, - int voxelBodies) { - return new BuildStats(geometry.scannedBlocks(), - geometry.solidBlocks(), - geometry.culledInteriorBlocks(), - geometry.mergedFullCubeBoxes().size(), - geometry.detailBoxCount(), - colliderBodies, - removedBodies, - sectionsBuilt, - sectionsRebuilt, - voxelBodies); - } - - @Nonnull - public BuildStats plus(@Nonnull BuildStats stats) { - return new BuildStats(scannedBlocks + stats.scannedBlocks, - solidBlocks + stats.solidBlocks, - culledInteriorBlocks + stats.culledInteriorBlocks, - fullCubeRuns + stats.fullCubeRuns, - detailBoxes + stats.detailBoxes, - colliderBodies + stats.colliderBodies, - removedBodies + stats.removedBodies, - sectionsBuilt + stats.sectionsBuilt, - sectionsRebuilt + stats.sectionsRebuilt, - voxelBodies + stats.voxelBodies); - } - - @Nonnull - private BuildStats withRemovedBodies(int removedBodies) { - return new BuildStats(scannedBlocks, - solidBlocks, - culledInteriorBlocks, - fullCubeRuns, - detailBoxes, - colliderBodies, - removedBodies, - sectionsBuilt, - sectionsRebuilt, - voxelBodies); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionCommand.java deleted file mode 100644 index 75fa2e36..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionCommand.java +++ /dev/null @@ -1,12 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.commands; - -import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection; - -public final class WorldCollisionCommand extends AbstractCommandCollection { - - public WorldCollisionCommand() { - super("worldcollision", "Impulse world-collision module commands"); - addSubCommand(new WorldCollisionSettingsCommand()); - addSubCommand(new WorldCollisionPerfCommand()); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionCommandContributions.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionCommandContributions.java deleted file mode 100644 index 405fed73..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionCommandContributions.java +++ /dev/null @@ -1,28 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.commands; - -import dev.hytalemodding.impulse.core.internal.commands.ImpulseCommandContributionRegistry; - -/** - * Command contributions owned by the world-collision subplugin. - */ -public final class WorldCollisionCommandContributions { - - private static final String ROOT_COMMAND_ID = "worldcollision.root"; - private static final String COLLISION_LOD_SETTINGS_ID = "worldcollision.settings.collision-lod"; - - private WorldCollisionCommandContributions() { - } - - public static void register() { - ImpulseCommandContributionRegistry.addRootAndSettingsSubCommands( - ROOT_COMMAND_ID, - WorldCollisionCommand::new, - COLLISION_LOD_SETTINGS_ID, - CollisionLodSettingsCommand::new); - } - - public static void unregister() { - ImpulseCommandContributionRegistry.removeRootAndSettingsSubCommands(ROOT_COMMAND_ID, - COLLISION_LOD_SETTINGS_ID); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionPerfCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionPerfCommand.java deleted file mode 100644 index 920bb2db..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionPerfCommand.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.commands; - -import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection; - -public final class WorldCollisionPerfCommand extends AbstractCommandCollection { - - public WorldCollisionPerfCommand() { - super("perf", "Impulse world-collision profiling commands"); - addSubCommand(new WorldCollisionPerfToggleCommand()); - addSubCommand(new WorldCollisionPerfReportCommand()); - addSubCommand(new WorldCollisionPerfResetCommand()); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionPerfResetCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionPerfResetCommand.java deleted file mode 100644 index 7a8e2291..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionPerfResetCommand.java +++ /dev/null @@ -1,31 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.commands; - -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.Message; -import com.hypixel.hytale.server.core.command.system.CommandContext; -import com.hypixel.hytale.server.core.command.system.basecommands.AbstractWorldCommand; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource; -import javax.annotation.Nonnull; - -public class WorldCollisionPerfResetCommand extends AbstractWorldCommand { - - public WorldCollisionPerfResetCommand() { - super("reset", "Reset Impulse runtime profiling counters"); - } - - @Override - protected void execute(@Nonnull CommandContext ctx, - @Nonnull World world, - @Nonnull Store store) { - PhysicsRuntimeProfilingResource runtimeProfiling = store.getResource( - PhysicsRuntimeProfilingResource.getResourceType()); - WorldCollisionProfilingResource profiling = store.getResource( - WorldCollisionProfilingResource.getResourceType()); - runtimeProfiling.reset(); - profiling.reset(); - ctx.sender().sendMessage(Message.raw("Impulse runtime profiling counters reset")); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionPerfToggleCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionPerfToggleCommand.java deleted file mode 100644 index 3e29f256..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionPerfToggleCommand.java +++ /dev/null @@ -1,33 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.commands; - -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.Message; -import com.hypixel.hytale.server.core.command.system.CommandContext; -import com.hypixel.hytale.server.core.command.system.basecommands.AbstractWorldCommand; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource; -import javax.annotation.Nonnull; - -public class WorldCollisionPerfToggleCommand extends AbstractWorldCommand { - - public WorldCollisionPerfToggleCommand() { - super("toggle", "Toggle Impulse runtime profiling"); - } - - @Override - protected void execute(@Nonnull CommandContext ctx, - @Nonnull World world, - @Nonnull Store store) { - PhysicsRuntimeProfilingResource runtimeProfiling = store.getResource( - PhysicsRuntimeProfilingResource.getResourceType()); - WorldCollisionProfilingResource profiling = store.getResource( - WorldCollisionProfilingResource.getResourceType()); - boolean enabled = !runtimeProfiling.isEnabled() || !profiling.isEnabled(); - runtimeProfiling.setEnabled(enabled); - profiling.setEnabled(enabled); - ctx.sender().sendMessage(Message.raw("Impulse runtime profiling " - + (enabled ? "enabled" : "disabled"))); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsChunkBoundarySystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsChunkBoundarySystem.java deleted file mode 100644 index 16add875..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsChunkBoundarySystem.java +++ /dev/null @@ -1,395 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems; - -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.dependency.Dependency; -import com.hypixel.hytale.component.dependency.Order; -import com.hypixel.hytale.component.dependency.SystemDependency; -import com.hypixel.hytale.component.dependency.SystemGroupDependency; -import com.hypixel.hytale.component.system.tick.TickingSystem; -import com.hypixel.hytale.math.util.ChunkUtil; -import com.hypixel.hytale.math.util.MathUtil; -import com.hypixel.hytale.server.core.modules.entity.system.UpdateLocationSystems; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.chunk.ChunkFlag; -import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk; -import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.PhysicsChunkBoundaryRuntime; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.PhysicsChunkBoundaryRuntime.ChunkBoundarySafeState; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldCollisionLifecycle; -import dev.hytalemodding.impulse.core.internal.systems.publication.PhysicsSnapshotPublicationSystem; -import dev.hytalemodding.impulse.core.internal.systems.sync.PhysicsSyncSystem; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerBridge; -import dev.hytalemodding.impulse.core.plugin.settings.EntityChunkBoundaryMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyRegistrationView; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import it.unimi.dsi.fastutil.longs.LongOpenHashSet; -import it.unimi.dsi.fastutil.longs.LongSet; -import java.util.Set; -import javax.annotation.Nonnull; -import org.joml.Vector2d; - -/** - * Keeps registered dynamic physics bodies from drifting into unloaded chunks. - * - *

The body key is the identity boundary here. Entity views may be absent, stale, - * or generated later, so this system uses the body's last known safe pose instead - * of entity transforms.

- */ -public class PhysicsChunkBoundarySystem extends TickingSystem { - - private final Set> dependencies = Set.of( - new SystemGroupDependency<>(Order.AFTER, ImpulsePlugin.get().getPersistenceRestoreGroup()), - new SystemDependency<>(Order.AFTER, PhysicsSnapshotPublicationSystem.class), - new SystemDependency<>(Order.BEFORE, PhysicsWorldCollisionStreamingSystem.class), - new SystemDependency<>(Order.BEFORE, PhysicsSyncSystem.class), - new SystemDependency<>(Order.BEFORE, UpdateLocationSystems.TickingSystem.class) - ); - - private static final int TICKING_CHUNK_REQUEST_FLAGS = 4; - private final LongSet requestedChunkIndices = new LongOpenHashSet(); - - @Override - public void tick(float dt, int systemIndex, @Nonnull Store store) { - if (!WorldCollisionLifecycle.isEnabled()) { - return; - } - requestedChunkIndices.clear(); - - World world = store.getExternalData().getWorld(); - ChunkStore chunkStore = world.getChunkStore(); - Store chunkComponentStore = chunkStore.getStore(); - PhysicsWorldRuntimeResource resource = PhysicsWorldRuntimeResource.require(store); - for (PhysicsBodyRegistrationView registration - : resource.getBodyRegistrationViews(PhysicsBodyKind.BODY)) { - processBody(registration, resource, store, chunkStore, chunkComponentStore); - } - } - - private void processBody(@Nonnull PhysicsBodyRegistrationView registration, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull Store store, - @Nonnull ChunkStore chunkStore, - @Nonnull Store chunkComponentStore) { - if (resource.getSpaceBinding(registration.spaceId()) == null) { - return; - } - - RigidBodyKey bodyKey = registration.bodyKey(); - PhysicsBodySnapshot snapshot = resource.getBodySnapshotIfRegistered(bodyKey); - if (snapshot == null) { - return; - } - if (snapshot.isStatic()) { - return; - } - - PhysicsSpaceSettings settings = resource.getLiveSpaceSettings(registration.spaceId()); - EntityChunkBoundaryMode mode = settings.getWorldCollisionSettings().getEntityChunkBoundaryMode(); - PhysicsChunkBoundaryRuntime.ChunkBoundaryPauseState pauseState = - resource.getChunkBoundaryPauseState(bodyKey); - if (pauseState != null) { - handlePausedBody(bodyKey, - snapshot, - pauseState, - mode, - resource, - store, - chunkStore, - chunkComponentStore); - return; - } - - long[] targetChunkIndices = chunkIndices(snapshot); - if (areChunksTicking(targetChunkIndices, chunkStore, chunkComponentStore)) { - recordSafePose(bodyKey, snapshot, resource); - return; - } - - if (mode == EntityChunkBoundaryMode.LOAD_TICKING_CHUNK) { - requestTickingChunks(chunkStore, targetChunkIndices); - return; - } - - PhysicsOwnerBridge.run(store, "pause chunk-boundary physics body", - () -> pauseBody(bodyKey, snapshot, targetChunkIndices, resource)); - } - - private void handlePausedBody(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull PhysicsChunkBoundaryRuntime.ChunkBoundaryPauseState pauseState, - @Nonnull EntityChunkBoundaryMode mode, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull Store entityStore, - @Nonnull ChunkStore chunkStore, - @Nonnull Store chunkComponentStore) { - long[] targetChunkIndices = pauseState.getTargetChunkIndices(); - if (mode == EntityChunkBoundaryMode.LOAD_TICKING_CHUNK) { - requestTickingChunks(chunkStore, targetChunkIndices); - } - - if (!areChunksTicking(targetChunkIndices, chunkStore, chunkComponentStore)) { - return; - } - - PhysicsOwnerBridge.run(entityStore, "resume chunk-boundary physics body", () -> { - var registration = resource.getRegistration(bodyKey); - if (registration == null) { - return; - } - PhysicsSpaceBinding space = resource.getSpaceBinding(registration.spaceId()); - if (space == null) { - return; - } - space.runtime() - .setBodyType(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value(), - BackendRuntimeCodes.bodyTypeCode( - pauseState.getOriginalBodyType())); - space.runtime().setBodyVelocity(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value(), - pauseState.getLinearVelocity().x, - pauseState.getLinearVelocity().y, - pauseState.getLinearVelocity().z, - pauseState.getAngularVelocity().x, - pauseState.getAngularVelocity().y, - pauseState.getAngularVelocity().z); - space.runtime().activateBody(space.backendSpaceHandle().value(), registration.backendBodyHandle().value()); - resource.clearChunkBoundaryPauseState(bodyKey); - recordSafePose(bodyKey, snapshot, resource); - }); - } - - static void pauseBody(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodySnapshot snapshot, - long targetChunkIndex, - @Nonnull PhysicsWorldRuntimeResource resource) { - pauseBody(bodyKey, snapshot, new long[] {targetChunkIndex}, resource); - } - - static void pauseBody(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull long[] targetChunkIndices, - @Nonnull PhysicsWorldRuntimeResource resource) { - var registration = resource.getRegistration(bodyKey); - if (registration == null) { - return; - } - PhysicsSpaceBinding space = resource.getSpaceBinding(registration.spaceId()); - if (space == null) { - return; - } - ChunkBoundarySafeState safeState = - resource.getChunkBoundarySafeState(bodyKey); - resource.pauseChunkBoundaryBody(bodyKey, - primaryChunkIndex(targetChunkIndices, snapshot), - targetChunkIndices, - snapshot); - - if (safeState != null) { - space.runtime().setBodyTransform(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value(), - safeState.getPosition().x, - safeState.getPosition().y, - safeState.getPosition().z, - safeState.getRotation().x, - safeState.getRotation().y, - safeState.getRotation().z, - safeState.getRotation().w); - } - - if (snapshot.bodyType() != PhysicsBodyType.KINEMATIC) { - space.runtime().setBodyType(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value(), - BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.KINEMATIC)); - } - space.runtime().setBodyVelocity(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value(), - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f); - } - - static void recordSafePose(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull PhysicsWorldRuntimeResource resource) { - resource.updateChunkBoundarySafeState(bodyKey, snapshot); - } - - private void requestTickingChunk(@Nonnull ChunkStore chunkStore, long chunkIndex) { - if (!requestedChunkIndices.add(chunkIndex)) { - return; - } - chunkStore.getChunkReferenceAsync(chunkIndex, TICKING_CHUNK_REQUEST_FLAGS); - } - - private void requestTickingChunks(@Nonnull ChunkStore chunkStore, - @Nonnull long[] chunkIndices) { - for (long chunkIndex : chunkIndices) { - requestTickingChunk(chunkStore, chunkIndex); - } - } - - private boolean areChunksTicking(@Nonnull long[] chunkIndices, - @Nonnull ChunkStore chunkStore, - @Nonnull Store chunkComponentStore) { - for (long chunkIndex : chunkIndices) { - if (!isChunkTicking(chunkIndex, chunkStore, chunkComponentStore)) { - return false; - } - } - return true; - } - - private boolean isChunkTicking(long chunkIndex, - @Nonnull ChunkStore chunkStore, - @Nonnull Store chunkComponentStore) { - var chunkRef = chunkStore.getChunkReference(chunkIndex); - if (chunkRef == null || !chunkRef.isValid()) { - return false; - } - - WorldChunk worldChunk = chunkComponentStore.getComponentConcurrent(chunkRef, - WorldChunk.getComponentType()); - return worldChunk != null && worldChunk.is(ChunkFlag.TICKING); - } - - private static long primaryChunkIndex(@Nonnull long[] chunkIndices, - @Nonnull PhysicsBodySnapshot snapshot) { - return chunkIndices.length > 0 - ? chunkIndices[0] - : chunkIndex(snapshot.positionX(), snapshot.positionZ()); - } - - static long[] chunkIndices(@Nonnull PhysicsBodySnapshot snapshot) { - Vector2d extents = horizontalHalfExtents(snapshot); - int minChunkX = chunkCoordinate(snapshot.positionX() - extents.x); - int maxChunkX = chunkCoordinate(snapshot.positionX() + extents.x); - int minChunkZ = chunkCoordinate(snapshot.positionZ() - extents.y); - int maxChunkZ = chunkCoordinate(snapshot.positionZ() + extents.y); - int count = (maxChunkX - minChunkX + 1) * (maxChunkZ - minChunkZ + 1); - long[] chunks = new long[count]; - int index = 0; - for (int chunkX = minChunkX; chunkX <= maxChunkX; chunkX++) { - for (int chunkZ = minChunkZ; chunkZ <= maxChunkZ; chunkZ++) { - chunks[index++] = ChunkUtil.indexChunk(chunkX, chunkZ); - } - } - return chunks; - } - - @Nonnull - private static Vector2d horizontalHalfExtents(@Nonnull PhysicsBodySnapshot snapshot) { - return switch (snapshot.shapeType()) { - case BOX -> boxHorizontalHalfExtents(snapshot); - case SPHERE -> roundHorizontalHalfExtents(snapshot.sphereRadius()); - case CAPSULE -> roundHeightHorizontalHalfExtents(snapshot, - finitePositive(snapshot.sphereRadius()), - finitePositive(snapshot.halfHeight()) + finitePositive(snapshot.sphereRadius())); - case CYLINDER, CONE -> roundHeightHorizontalHalfExtents(snapshot, - finitePositive(snapshot.sphereRadius()), - finitePositive(snapshot.halfHeight())); - case PLANE, VOXELS, UNKNOWN -> new Vector2d(); - }; - } - - @Nonnull - private static Vector2d boxHorizontalHalfExtents(@Nonnull PhysicsBodySnapshot snapshot) { - if (!snapshot.hasBoxHalfExtents()) { - return new Vector2d(); - } - return rotatedHorizontalHalfExtents(snapshot, - finitePositive(snapshot.boxHalfExtentX()), - finitePositive(snapshot.boxHalfExtentY()), - finitePositive(snapshot.boxHalfExtentZ())); - } - - @Nonnull - private static Vector2d roundHorizontalHalfExtents(float radius) { - double halfExtent = finitePositive(radius); - return new Vector2d(halfExtent, halfExtent); - } - - @Nonnull - private static Vector2d roundHeightHorizontalHalfExtents(@Nonnull PhysicsBodySnapshot snapshot, - double radius, - double axisHalfExtent) { - double halfX = radius; - double halfY = radius; - double halfZ = radius; - switch (snapshot.shapeAxis()) { - case X -> halfX = axisHalfExtent; - case Y -> halfY = axisHalfExtent; - case Z -> halfZ = axisHalfExtent; - } - return rotatedHorizontalHalfExtents(snapshot, halfX, halfY, halfZ); - } - - @Nonnull - private static Vector2d rotatedHorizontalHalfExtents(@Nonnull PhysicsBodySnapshot snapshot, - double halfX, - double halfY, - double halfZ) { - double x = snapshot.rotationX(); - double y = snapshot.rotationY(); - double z = snapshot.rotationZ(); - double w = snapshot.rotationW(); - double lengthSquared = x * x + y * y + z * z + w * w; - if (!Double.isFinite(lengthSquared) || lengthSquared <= 0.0) { - return new Vector2d(halfX, halfZ); - } - - double scale = 2.0 / lengthSquared; - double xs = x * scale; - double ys = y * scale; - double zs = z * scale; - double wx = w * xs; - double wy = w * ys; - double wz = w * zs; - double xx = x * xs; - double xy = x * ys; - double xz = x * zs; - double yy = y * ys; - double yz = y * zs; - double zz = z * zs; - double m00 = 1.0 - (yy + zz); - double m01 = xy - wz; - double m02 = xz + wy; - double m20 = xz - wy; - double m21 = yz + wx; - double m22 = 1.0 - (xx + yy); - return new Vector2d(Math.abs(m00) * halfX + Math.abs(m01) * halfY + Math.abs(m02) * halfZ, - Math.abs(m20) * halfX + Math.abs(m21) * halfY + Math.abs(m22) * halfZ); - } - - private static double finitePositive(float value) { - return Float.isFinite(value) && value > 0.0f ? value : 0.0; - } - - private static int chunkCoordinate(double coordinate) { - return MathUtil.floor(coordinate) >> ChunkUtil.BITS; - } - - private static long chunkIndex(double x, double z) { - int chunkX = MathUtil.floor(x) >> ChunkUtil.BITS; - int chunkZ = MathUtil.floor(z) >> ChunkUtil.BITS; - return ChunkUtil.indexChunk(chunkX, chunkZ); - } - - @Nonnull - @Override - public Set> getDependencies() { - return dependencies; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsCollisionLodSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsCollisionLodSystem.java deleted file mode 100644 index 2b25644f..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsCollisionLodSystem.java +++ /dev/null @@ -1,440 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems; - -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.dependency.Dependency; -import com.hypixel.hytale.component.dependency.Order; -import com.hypixel.hytale.component.dependency.SystemDependency; -import com.hypixel.hytale.component.system.tick.TickingSystem; -import com.hypixel.hytale.logger.HytaleLogger; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodySnapshots; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime; -import dev.hytalemodding.impulse.core.internal.systems.visual.VisualInterestCollector; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerBridge; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldCollisionLifecycle; -import dev.hytalemodding.impulse.core.internal.systems.publication.PhysicsSnapshotPublicationSystem; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistration; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import it.unimi.dsi.fastutil.ints.Int2LongOpenHashMap; -import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -import it.unimi.dsi.fastutil.objects.Object2ObjectMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.logging.Level; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3f; - -/** - * Applies an opt-in distance collision LOD for default Impulse dynamic-body filters. - */ -public class PhysicsCollisionLodSystem extends TickingSystem { - - private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); - private static final Set> DEPENDENCIES = Set.of( - new SystemDependency<>(Order.AFTER, PhysicsSnapshotPublicationSystem.class) - ); - - @Nonnull - private final Map, CollisionLodState> statesByStore = - Collections.synchronizedMap(new WeakHashMap<>()); - - @Override - public void tick(float dt, int systemIndex, @Nonnull Store store) { - if (!WorldCollisionLifecycle.isEnabled()) { - return; - } - PhysicsWorldRuntimeResource resource = PhysicsWorldRuntimeResource.require(store); - CollisionLodState state = stateFor(store); - state.refreshPendingMutation(); - if (state.hasPendingMutation()) { - return; - } - - long tick = state.nextTick(); - List updates = collectUpdates(store, resource, state, tick); - if (updates.isEmpty()) { - return; - } - - PhysicsMutationHandle handle = PhysicsOwnerBridge.runAsync(store, - "apply collision LOD filters", - () -> applyUpdates(resource, updates)); - state.trackPendingMutation(handle, updates); - } - - @Nonnull - private List collectUpdates(@Nonnull Store store, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull CollisionLodState state, - long tick) { - List updates = new ArrayList<>(); - IntOpenHashSet activeSpaces = new IntOpenHashSet(); - List interests = - VisualInterestCollector.collectMaterializationInterests(store, resource); - for (PhysicsSpaceBinding space : resource.getSpaceBindings()) { - SpaceId spaceId = space.spaceId(); - activeSpaces.add(spaceId.value()); - PhysicsSpaceSettings settings = resource.getLiveSpaceSettings(spaceId); - if (!settings.getCollisionLodSettings().isCollisionLodEnabled()) { - state.collectRestoreUpdates(spaceId, updates); - continue; - } - if (!state.shouldRefresh(spaceId, - settings.getCollisionLodSettings().getCollisionLodRefreshIntervalTicks(), - tick)) { - continue; - } - collectSpaceUpdates(resource, spaceId, settings, interests, state, updates); - } - state.pruneRemovedSpaces(activeSpaces); - return updates; - } - - private static void collectSpaceUpdates(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceSettings settings, - @Nonnull List interests, - @Nonnull CollisionLodState state, - @Nonnull List updates) { - ObjectOpenHashSet seenBodies = new ObjectOpenHashSet<>(); - resource.forEachIndexedBodySnapshot(spaceId, (bodyKey, snapshot, _, kind, persistenceMode) -> { - seenBodies.add(bodyKey); - if (persistenceMode == PhysicsBodyPersistenceMode.PERSISTENT) { - state.recordRestore(spaceId, bodyKey, updates); - return; - } - if (!isCollisionLodCandidate(snapshot, kind, persistenceMode)) { - return; - } - - CollisionLodTier previousTier = state.tier(bodyKey); - CollisionLodTier tier = resource.isBodyControlled(bodyKey) - ? CollisionLodTier.NEAR_FULL - : resolveTier(settings, - previousTier, - snapshot.positionX(), - snapshot.positionY(), - snapshot.positionZ(), - interests); - state.recordTier(spaceId, bodyKey, tier, updates); - }); - state.pruneMissingBodies(spaceId, seenBodies); - } - - static CollisionLodTier resolveTier(@Nonnull PhysicsSpaceSettings settings, - @Nullable CollisionLodTier previousTier, - @Nonnull Vector3f position, - @Nonnull List interests) { - return resolveTier(settings, previousTier, position.x, position.y, position.z, interests); - } - - static CollisionLodTier resolveTier(@Nonnull PhysicsSpaceSettings settings, - @Nullable CollisionLodTier previousTier, - float positionX, - float positionY, - float positionZ, - @Nonnull List interests) { - float distanceSquared = nearestDistanceSquared(positionX, positionY, positionZ, interests); - if (distanceSquared == Float.POSITIVE_INFINITY) { - return CollisionLodTier.FAR_SLEEPING; - } - - int nearRadius = settings.getCollisionLodSettings().getCollisionLodNearRadius(); - int midRadius = settings.getCollisionLodSettings().getCollisionLodMidRadius(); - int hysteresis = settings.getCollisionLodSettings().getCollisionLodHysteresis(); - if (previousTier == CollisionLodTier.NEAR_FULL) { - return distanceSquared <= squared(nearRadius + hysteresis) - ? CollisionLodTier.NEAR_FULL - : resolveTierWithoutHysteresis(distanceSquared, nearRadius, midRadius); - } - if (previousTier == CollisionLodTier.MID_TERRAIN) { - if (distanceSquared <= squared(nearRadius)) { - return CollisionLodTier.NEAR_FULL; - } - return distanceSquared <= squared(midRadius + hysteresis) - ? CollisionLodTier.MID_TERRAIN - : CollisionLodTier.FAR_SLEEPING; - } - return resolveTierWithoutHysteresis(distanceSquared, nearRadius, midRadius); - } - - @Nonnull - private static CollisionLodTier resolveTierWithoutHysteresis(float distanceSquared, - int nearRadius, - int midRadius) { - if (distanceSquared <= squared(nearRadius)) { - return CollisionLodTier.NEAR_FULL; - } - if (distanceSquared <= squared(midRadius)) { - return CollisionLodTier.MID_TERRAIN; - } - return CollisionLodTier.FAR_SLEEPING; - } - - private static float nearestDistanceSquared(float positionX, - float positionY, - float positionZ, - @Nonnull List interests) { - float nearest = Float.POSITIVE_INFINITY; - for (PhysicsVisualRuntime.VisualInterest interest : interests) { - Vector3f interestPosition = interest.position(); - float dx = positionX - interestPosition.x; - float dy = positionY - interestPosition.y; - float dz = positionZ - interestPosition.z; - nearest = Math.min(nearest, dx * dx + dy * dy + dz * dz); - } - return nearest; - } - - private static int squared(int value) { - return value * value; - } - - static boolean isCollisionLodCandidate(@Nonnull PhysicsBodySnapshot snapshot, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - return persistenceMode != PhysicsBodyPersistenceMode.PERSISTENT - && kind == PhysicsBodyKind.BODY - && snapshot.isDynamic() - && !snapshot.sensor(); - } - - private static void applyUpdates(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull List updates) { - for (CollisionLodUpdate update : updates) { - PhysicsBodyRegistration registration = - resource.getRegistration(update.bodyKey()); - if (registration == null - || !registration.spaceId().equals(update.spaceId()) - || registration.kind() != PhysicsBodyKind.BODY - || (update.trackTier() - && registration.persistenceMode() == PhysicsBodyPersistenceMode.PERSISTENT)) { - continue; - } - PhysicsSpaceBinding space = resource.getSpaceBinding(registration.spaceId()); - if (space == null) { - continue; - } - PhysicsBodySnapshot snapshot = PhysicsBodySnapshots.read(space, registration.backendBodyHandle().value()); - if (snapshot == null || !snapshot.isDynamic() || snapshot.sensor()) { - continue; - } - PhysicsSpaceSettings settings = resource.getLiveSpaceSettings(update.spaceId()); - applyTier(space, - registration.backendBodyHandle().value(), - update.tier(), - settings.getCollisionLodSettings().isCollisionLodFarSleepEnabled()); - } - } - - private static void applyTier(@Nonnull PhysicsSpaceBinding space, - long backendBodyId, - @Nonnull CollisionLodTier tier, - boolean farSleepEnabled) { - int terrainOnlyMask = PhysicsCollisionFilters.TERRAIN; - int fullDynamicMask = PhysicsCollisionFilters.TERRAIN - | PhysicsCollisionFilters.DYNAMIC_BODY; - switch (tier) { - case NEAR_FULL -> { - space.runtime().setBodyCollisionFilter(space.backendSpaceHandle().value(), - backendBodyId, - PhysicsCollisionFilters.DYNAMIC_BODY, - fullDynamicMask); - space.runtime().activateBody(space.backendSpaceHandle().value(), backendBodyId); - } - case MID_TERRAIN -> { - space.runtime().setBodyCollisionFilter(space.backendSpaceHandle().value(), - backendBodyId, - PhysicsCollisionFilters.DYNAMIC_BODY, - terrainOnlyMask); - space.runtime().activateBody(space.backendSpaceHandle().value(), backendBodyId); - } - case FAR_SLEEPING -> { - space.runtime().setBodyCollisionFilter(space.backendSpaceHandle().value(), - backendBodyId, - PhysicsCollisionFilters.DYNAMIC_BODY, - terrainOnlyMask); - if (farSleepEnabled) { - space.runtime().sleepBody(space.backendSpaceHandle().value(), backendBodyId); - } - } - } - } - - @Nonnull - private CollisionLodState stateFor(@Nonnull Store store) { - synchronized (statesByStore) { - return statesByStore.computeIfAbsent(store, _ -> new CollisionLodState()); - } - } - - enum CollisionLodTier { - NEAR_FULL, - MID_TERRAIN, - FAR_SLEEPING - } - - record CollisionLodUpdate(@Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyKey, - @Nonnull CollisionLodTier tier, - boolean trackTier) { - } - - private record BodyTier(@Nonnull SpaceId spaceId, @Nonnull CollisionLodTier tier) { - } - - static final class CollisionLodState { - - @Nonnull - private final Object2ObjectMap tiers = - new Object2ObjectOpenHashMap<>(); - @Nonnull - private final Int2LongOpenHashMap nextRefreshTicks = new Int2LongOpenHashMap(); - @Nonnull - private final Queue pendingUpdates = new ArrayDeque<>(); - @Nullable - private PhysicsMutationHandle pendingHandle; - private long tick; - - CollisionLodState() { - nextRefreshTicks.defaultReturnValue(0L); - } - - private long nextTick() { - return ++tick; - } - - boolean hasPendingMutation() { - return pendingHandle != null; - } - - void refreshPendingMutation() { - if (pendingHandle == null || !pendingHandle.isDone()) { - return; - } - Throwable failure = pendingHandle.failure(); - if (failure != null) { - LOGGER.at(Level.WARNING) - .log("Collision LOD owner mutation failed: %s", failure.getMessage()); - clearPendingRefreshes(); - } else { - commitPendingUpdates(); - } - pendingUpdates.clear(); - pendingHandle = null; - } - - void trackPendingMutation(@Nonnull PhysicsMutationHandle handle, - @Nonnull List updates) { - pendingHandle = handle; - pendingUpdates.addAll(updates); - } - - boolean shouldRefresh(@Nonnull SpaceId spaceId, int interval, long tick) { - long nextRefreshTick = nextRefreshTicks.get(spaceId.value()); - if (tick < nextRefreshTick) { - return false; - } - nextRefreshTicks.put(spaceId.value(), tick + interval); - return true; - } - - @Nullable - CollisionLodTier tier(@Nonnull RigidBodyKey bodyKey) { - BodyTier bodyTier = tiers.get(bodyKey); - return bodyTier != null ? bodyTier.tier() : null; - } - - void recordTier(@Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyKey, - @Nonnull CollisionLodTier tier, - @Nonnull List updates) { - BodyTier previous = tiers.get(bodyKey); - if (previous != null && previous.spaceId().equals(spaceId) && previous.tier() == tier) { - return; - } - updates.add(new CollisionLodUpdate(spaceId, bodyKey, tier, true)); - } - - void recordRestore(@Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyKey, - @Nonnull List updates) { - BodyTier previous = tiers.get(bodyKey); - if (previous == null || !previous.spaceId().equals(spaceId)) { - return; - } - updates.add(new CollisionLodUpdate(spaceId, - bodyKey, - CollisionLodTier.NEAR_FULL, - false)); - } - - private void collectRestoreUpdates(@Nonnull SpaceId spaceId, - @Nonnull List updates) { - for (Object2ObjectMap.Entry entry - : tiers.object2ObjectEntrySet()) { - if (!entry.getValue().spaceId().equals(spaceId)) { - continue; - } - updates.add(new CollisionLodUpdate(spaceId, - entry.getKey(), - CollisionLodTier.NEAR_FULL, - false)); - } - nextRefreshTicks.remove(spaceId.value()); - } - - private void commitPendingUpdates() { - for (CollisionLodUpdate update : pendingUpdates) { - if (update.trackTier()) { - tiers.put(update.bodyKey(), new BodyTier(update.spaceId(), update.tier())); - } else { - tiers.remove(update.bodyKey()); - } - } - } - - private void clearPendingRefreshes() { - for (CollisionLodUpdate update : pendingUpdates) { - nextRefreshTicks.remove(update.spaceId().value()); - } - } - - private void pruneMissingBodies(@Nonnull SpaceId spaceId, - @Nonnull ObjectOpenHashSet seenBodies) { - tiers.object2ObjectEntrySet() - .removeIf(entry -> entry.getValue().spaceId().equals(spaceId) - && !seenBodies.contains(entry.getKey())); - } - - private void pruneRemovedSpaces(@Nonnull IntOpenHashSet activeSpaces) { - tiers.object2ObjectEntrySet() - .removeIf(entry -> !activeSpaces.contains(entry.getValue().spaceId().value())); - nextRefreshTicks.keySet().removeIf(spaceValue -> !activeSpaces.contains(spaceValue)); - } - } - - @Nonnull - @Override - public Set> getDependencies() { - return DEPENDENCIES; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsWorldCollisionStreamingSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsWorldCollisionStreamingSystem.java deleted file mode 100644 index 1a3cb0a4..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsWorldCollisionStreamingSystem.java +++ /dev/null @@ -1,549 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems; - -import com.hypixel.hytale.component.ArchetypeChunk; -import com.hypixel.hytale.component.CommandBuffer; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.dependency.Dependency; -import com.hypixel.hytale.component.dependency.Order; -import com.hypixel.hytale.component.dependency.SystemDependency; -import com.hypixel.hytale.component.query.Query; -import com.hypixel.hytale.component.system.QuerySystem; -import com.hypixel.hytale.component.system.tick.TickingSystem; -import com.hypixel.hytale.server.core.entity.entities.Player; -import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource.Snapshot; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource.StreamingTargetDiagnostic; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldCollisionLifecycle; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldCollisionBuildOptions; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldCollisionStreamingBounds; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldVoxelCollisionCache; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldVoxelCollisionCache.SectionAccessCache; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldVoxelCollisionCache.TargetRefreshDecision; -import dev.hytalemodding.impulse.core.internal.systems.publication.PhysicsSnapshotPublicationSystem; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionMode; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldCollisionSettings; -import it.unimi.dsi.fastutil.longs.LongOpenHashSet; -import it.unimi.dsi.fastutil.longs.LongSet; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.BiConsumer; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3d; - -/** - * Streams world voxel collision around online players and dynamic physics bodies - * once per entity-store tick. - * - *

Players stream at the configured world collision radius. - * Dynamic physics bodies stream at a smaller radius so they do not - * aggressively pull collision into unpopulated areas, but still - * have terrain to interact with after rolling away from players.

- */ -public class PhysicsWorldCollisionStreamingSystem extends TickingSystem - implements QuerySystem { - - @Nullable - private static volatile ComponentType playerType; - @Nullable - private static volatile ComponentType transformType; - @Nullable - private static volatile Query query; - private static final Set> DEPENDENCIES = Set.of( - new SystemDependency<>(Order.AFTER, PhysicsSnapshotPublicationSystem.class) - ); - - /** - * Radius (in blocks) used for streaming around dynamic physics bodies. - * Smaller than the player radius because bodies should not pull collision - * as far as players do, but they still need terrain to land on. - */ - public static final int DEFAULT_BODY_STREAMING_RADIUS = 4; - - @Nonnull - private final Map, StreamingState> statesByStore = - Collections.synchronizedMap(new WeakHashMap<>()); - - @Override - public void tick(float dt, int systemIndex, @Nonnull Store store) { - if (!WorldCollisionLifecycle.isEnabled()) { - return; - } - WorldCollisionProfilingResource profiling = store.getResource( - WorldCollisionProfilingResource.getResourceType()); - Snapshot snapshot = null; - long tickStart = 0L; - try { - World world = store.getExternalData().getWorld(); - PhysicsWorldRuntimeResource resource = PhysicsWorldRuntimeResource.require(store); - WorldVoxelCollisionCache cache = resource.worldCollisionCache(); - if (cache.isStreamingApplyPending()) { - recordSkippedTerrainApply(profiling); - return; - } - - snapshot = profiling.isEnabled() ? profiling.beginTick() : null; - tickStart = snapshot != null ? System.nanoTime() : 0L; - List playerPositions = new ArrayList<>(); - BiConsumer, CommandBuffer> collector = - (chunk, _) -> collectPlayerPositions(chunk, playerPositions); - store.forEachChunk(systemIndex, collector); - List streamingPlayerPositions = List.copyOf(playerPositions); - - if (snapshot != null) { - snapshot.setPlayerStreamingTargets(streamingPlayerPositions.size()); - } - - long currentTick = stateFor(store).nextTick(); - List plans = collectStreamingPlans(resource, - cache, - streamingPlayerPositions, - currentTick, - snapshot); - if (plans.isEmpty()) { - return; - } - if (!cache.tryBeginStreamingApply()) { - if (snapshot != null) { - snapshot.incrementTerrainApplySkippedPending(); - } - return; - } - Snapshot applySnapshot = snapshot; - long queuedStart = tickStart; - if (tryQueueStreamingApply(cache, profiling, applySnapshot, queuedStart, applyFinished -> - resource.enqueueOwnerMutation("stream world collision terrain apply", () -> { - long applyStart = applySnapshot != null ? System.nanoTime() : 0L; - try { - SectionAccessCache sectionAccessCache = cache.newSectionAccessCache(); - for (SpaceStreamingPlan plan : plans) { - PhysicsSpaceBinding space = resource.getSpaceBinding(plan.spaceId()); - if (space == null) { - continue; - } - PhysicsSpaceSettings settings = resource.getLiveSpaceSettings(plan.spaceId()); - PhysicsWorldCollisionSettings collisionSettings = - settings.getWorldCollisionSettings(); - if (!WorldCollisionLifecycle.isEnabled() - || WorldCollisionLifecycle.generation() != plan.lifecycleGeneration() - || collisionSettings.getWorldCollisionMode() != WorldCollisionMode.STREAMING - || resource.worldCollisionStreamingRevision(plan.spaceId()) != plan.settingsRevision()) { - continue; - } - applySpaceCollision(world, - cache, - sectionAccessCache, - space, - plan, - collisionSettings, - currentTick, - applySnapshot); - } - } finally { - finishQueuedStreamingApply(cache, - profiling, - applySnapshot, - applyStart, - applyFinished); - } - }))) { - snapshot = null; - } - } finally { - if (snapshot != null) { - snapshot.setTickNanos(System.nanoTime() - tickStart); - profiling.finishTick(snapshot); - } - } - } - - static boolean tryQueueStreamingApply(@Nonnull WorldVoxelCollisionCache cache, - @Nonnull WorldCollisionProfilingResource profiling, - @Nullable Snapshot snapshot, - long queuedStartNanos, - @Nonnull StreamingApplySubmission submission) { - AtomicBoolean applyFinished = new AtomicBoolean(); - try { - PhysicsMutationHandle handle = submission.submit(applyFinished); - if (isRejectedExecutionFailure(handle.failure())) { - finishRejectedStreamingApply(cache, snapshot); - return false; - } - if (snapshot != null) { - snapshot.incrementTerrainApplyQueued(); - } - handle.completion().exceptionally(failure -> { - finishQueuedStreamingApply(cache, - profiling, - snapshot, - queuedStartNanos, - applyFinished); - return null; - }); - return true; - } catch (RejectedExecutionException exception) { - finishRejectedStreamingApply(cache, snapshot); - return false; - } - } - - static void finishQueuedStreamingApply(@Nonnull WorldVoxelCollisionCache cache, - @Nonnull WorldCollisionProfilingResource profiling, - @Nullable Snapshot snapshot, - long startNanos, - @Nonnull AtomicBoolean finished) { - if (!finished.compareAndSet(false, true)) { - return; - } - if (snapshot != null) { - snapshot.setTickNanos(System.nanoTime() - startNanos); - profiling.finishTick(snapshot); - } - cache.finishStreamingApply(); - } - - static void finishRejectedStreamingApply(@Nonnull WorldVoxelCollisionCache cache, - @Nullable Snapshot snapshot) { - cache.finishStreamingApply(); - if (snapshot != null) { - snapshot.incrementTerrainApplySkippedPending(); - } - } - - @FunctionalInterface - interface StreamingApplySubmission { - - @Nonnull - PhysicsMutationHandle submit(@Nonnull AtomicBoolean applyFinished); - } - - private static boolean isRejectedExecutionFailure(@Nullable Throwable failure) { - Throwable current = failure; - while (current != null) { - if (current instanceof RejectedExecutionException) { - return true; - } - current = current.getCause(); - } - return false; - } - - private static void recordSkippedTerrainApply( - @Nonnull WorldCollisionProfilingResource profiling) { - if (!profiling.isEnabled()) { - return; - } - Snapshot snapshot = profiling.beginTick(); - snapshot.incrementTerrainApplySkippedPending(); - profiling.finishTick(snapshot); - } - - @Nonnull - private List collectStreamingPlans( - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull WorldVoxelCollisionCache cache, - @Nonnull List playerPositions, - long currentTick, - @Nullable Snapshot snapshot) { - List plans = new ArrayList<>(); - if (!WorldCollisionLifecycle.isEnabled()) { - return plans; - } - long lifecycleGeneration = WorldCollisionLifecycle.generation(); - for (SpaceId spaceId : resource.getSpaceIds()) { - PhysicsSpaceSettings settings = resource.getLiveSpaceSettings(spaceId); - if (settings.getWorldCollisionSettings().getWorldCollisionMode() != WorldCollisionMode.STREAMING) { - continue; - } - - if (snapshot != null) { - snapshot.incrementStreamingSpaces(); - } - - int bodyRadius = settings.getWorldCollisionSettings().getWorldCollisionBodyRadius(); - plans.add(new SpaceStreamingPlan(spaceId, - resource.worldCollisionStreamingRevision(spaceId), - lifecycleGeneration, - playerPositions, - collectDynamicBodyTargets(resource, - cache, - spaceId, - bodyRadius, - currentTick, - settings.getWorldCollisionSettings().getWorldCollisionTtlTicks(), - snapshot))); - } - return plans; - } - - private void applySpaceCollision(@Nonnull World world, - @Nonnull WorldVoxelCollisionCache cache, - @Nonnull SectionAccessCache sectionAccessCache, - @Nonnull PhysicsSpaceBinding space, - @Nonnull SpaceStreamingPlan plan, - @Nonnull PhysicsWorldCollisionSettings settings, - long currentTick, - @Nullable Snapshot snapshot) { - LongSet visitedSections = new LongOpenHashSet(); - WorldCollisionBuildOptions buildOptions = WorldCollisionBuildOptions.fromSettings(settings); - for (Vector3d position : plan.playerPositions()) { - int sectionsBefore = visitedSections.size(); - cache.ensureAround(world, - space, - position, - settings.getWorldCollisionRadius(), - currentTick, - snapshot, - visitedSections, - snapshot != null ? StreamingTargetDiagnostic.player(position) : null, - sectionAccessCache, - buildOptions); - if (snapshot != null) { - snapshot.addPlayerSectionTargets(visitedSections.size() - sectionsBefore); - } - } - for (BodyStreamingTarget target : plan.bodyTargets()) { - int sectionsBefore = visitedSections.size(); - cache.ensureAround(world, - space, - target.position(), - settings.getWorldCollisionBodyRadius(), - currentTick, - snapshot, - visitedSections, - target.diagnostic(), - sectionAccessCache, - buildOptions); - for (BodyStreamingRefresh refresh : target.refreshes()) { - cache.recordBodyTargetRefresh(space.spaceId(), - refresh.bodyKey(), - target.bounds(), - refresh.sleeping(), - currentTick); - } - if (snapshot != null) { - snapshot.addBodySectionTargets(visitedSections.size() - sectionsBefore); - } - } - cache.pruneUnloaded(world, space.spaceId(), space, snapshot, sectionAccessCache); - cache.pruneUnused(space.spaceId(), - space, - currentTick, - settings.getWorldCollisionTtlTicks(), - snapshot); - } - - private void collectPlayerPositions(@Nonnull ArchetypeChunk chunk, - @Nonnull List positions) { - for (int index = 0; index < chunk.size(); index++) { - TransformComponent transform = chunk.getComponent(index, transformType()); - if (transform != null) { - positions.add(new Vector3d(transform.getPosition())); - } - } - } - - /** - * Collects unique dynamic-body streaming targets for one physics space. - * - *

Bodies are deduplicated by the exact section bounds that the configured - * radius would touch. This removes a large amount of repeated `ensureAround` - * work for big piles where many bodies share the same neighborhood.

- */ - @Nonnull - private List collectDynamicBodyTargets(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull WorldVoxelCollisionCache cache, - @Nonnull SpaceId spaceId, - int radius, - long currentTick, - int ttlTicks, - @Nullable Snapshot snapshot) { - Map uniqueTargets = - new Object2ObjectOpenHashMap<>(); - BodyStreamingCollectionStats stats = new BodyStreamingCollectionStats(); - resource.forEachIndexedBodySnapshot(spaceId, (bodyKey, bodySnapshot, bodySpaceId, kind, persistenceMode) -> { - stats.spatialIndexCandidateCount++; - if (!bodySnapshot.isDynamic()) { - return; - } - - stats.candidateCount++; - float positionX = bodySnapshot.positionX(); - float positionY = bodySnapshot.positionY(); - float positionZ = bodySnapshot.positionZ(); - WorldCollisionStreamingBounds bounds = WorldCollisionStreamingBounds.from(positionX, - positionY, - positionZ, - radius); - TargetRefreshDecision refreshDecision = cache.shouldRefreshBodyTarget(spaceId, - bodyKey, - bounds, - bodySnapshot.sleeping(), - currentTick, - ttlTicks, - snapshot); - if (!refreshDecision.refresh()) { - return; - } - BodyStreamingTarget previous = uniqueTargets.get(bounds); - if (previous == null) { - BodyStreamingTarget target = new BodyStreamingTarget( - new Vector3d(positionX, positionY, positionZ), - bounds, - new ArrayList<>(), - diagnosticFor(snapshot, bodyKey, positionX, positionY, positionZ)); - target.refreshes().add(new BodyStreamingRefresh(bodyKey, bodySnapshot.sleeping())); - uniqueTargets.put(bounds, target); - } - if (previous != null) { - previous.refreshes().add(new BodyStreamingRefresh(bodyKey, bodySnapshot.sleeping())); - if (snapshot != null) { - snapshot.incrementBodyTargetDedupeSkips(); - } - } - }); - cache.pruneBodyStreamingTargets(spaceId, currentTick, ttlTicks, snapshot); - if (snapshot != null) { - snapshot.addBodyStreamingCandidates(stats.candidateCount); - snapshot.addBodySpatialIndexCandidates(stats.spatialIndexCandidateCount); - snapshot.addBodyStreamingTargets(uniqueTargets.size()); - } - return new ArrayList<>(uniqueTargets.values()); - } - - private static final class BodyStreamingCollectionStats { - - private int spatialIndexCandidateCount; - private int candidateCount; - } - - @Nullable - private static StreamingTargetDiagnostic diagnosticFor(@Nullable Snapshot snapshot, - @Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ) { - if (snapshot == null) { - return null; - } - - return StreamingTargetDiagnostic.body(bodyKey, - positionX, - positionY, - positionZ, - positionX, - positionY, - positionZ); - } - - @Nonnull - private StreamingState stateFor(@Nonnull Store store) { - synchronized (statesByStore) { - return statesByStore.computeIfAbsent(store, _ -> new StreamingState()); - } - } - - private record BodyStreamingTarget(@Nonnull Vector3d position, - @Nonnull WorldCollisionStreamingBounds bounds, - @Nonnull List refreshes, - @Nullable StreamingTargetDiagnostic diagnostic) { - } - - private record BodyStreamingRefresh(@Nonnull RigidBodyKey bodyKey, - boolean sleeping) { - } - - private record SpaceStreamingPlan(@Nonnull SpaceId spaceId, - long settingsRevision, - long lifecycleGeneration, - @Nonnull List playerPositions, - @Nonnull List bodyTargets) { - } - - private static final class StreamingState { - - private long tick; - - private synchronized long nextTick() { - return ++tick; - } - } - - @Nonnull - @Override - public Query getQuery() { - return query(); - } - - @Nonnull - @Override - public Set> getDependencies() { - return DEPENDENCIES; - } - - @Nonnull - private static Query query() { - Query resolved = query; - if (resolved != null) { - return resolved; - } - synchronized (PhysicsWorldCollisionStreamingSystem.class) { - resolved = query; - if (resolved == null) { - resolved = Query.and(playerType(), transformType()); - query = resolved; - } - } - return resolved; - } - - @Nonnull - private static ComponentType playerType() { - ComponentType resolved = playerType; - if (resolved != null) { - return resolved; - } - synchronized (PhysicsWorldCollisionStreamingSystem.class) { - resolved = playerType; - if (resolved == null) { - resolved = Player.getComponentType(); - playerType = resolved; - } - } - return resolved; - } - - @Nonnull - private static ComponentType transformType() { - ComponentType resolved = transformType; - if (resolved != null) { - return resolved; - } - synchronized (PhysicsWorldCollisionStreamingSystem.class) { - resolved = transformType; - if (resolved == null) { - resolved = TransformComponent.getComponentType(); - transformType = resolved; - } - } - return resolved; - } - -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/package-info.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/package-info.java index 08e3483f..71f73d14 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/package-info.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/package-info.java @@ -2,7 +2,7 @@ * Internal Impulse Core implementation packages. * *

Types under this package tree support the Hytale runtime integration, - * commands, persistence, diagnostics, world collision, and system execution. + * commands, persistence, diagnostics, PhysicsChunk collision, and system execution. * They are not the supported third-party plugin API.

*/ package dev.hytalemodding.impulse.core.internal; diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsBinaryPayloadCodec.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsBinaryPayloadCodec.java deleted file mode 100644 index 056effd1..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsBinaryPayloadCodec.java +++ /dev/null @@ -1,124 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.persistence; - -import com.hypixel.hytale.codec.Codec; -import com.hypixel.hytale.codec.ExtraInfo; -import com.hypixel.hytale.codec.exception.CodecException; -import com.hypixel.hytale.codec.schema.SchemaContext; -import com.hypixel.hytale.codec.schema.config.Schema; -import com.hypixel.hytale.codec.schema.config.StringSchema; -import com.hypixel.hytale.codec.util.RawJsonReader; -import java.io.IOException; -import java.util.Arrays; -import java.util.Base64; -import javax.annotation.Nonnull; -import org.bson.BsonBinary; -import org.bson.BsonValue; - -/** - * Binary payload codec for compressed physics state blocks. - */ -final class PersistentPhysicsBinaryPayloadCodec implements Codec { - - private static final byte[] EMPTY_PAYLOAD = new byte[0]; - - @Nonnull - @Override - public byte[] decode(@Nonnull BsonValue bsonValue, ExtraInfo extraInfo) { - return copyPayload(bsonValue.asBinary().getData()); - } - - @Nonnull - @Override - public BsonValue encode(@Nonnull byte[] bytes, ExtraInfo extraInfo) { - return new BsonBinary(bytes); - } - - @Nonnull - @Override - public byte[] decodeJson(@Nonnull RawJsonReader reader, ExtraInfo extraInfo) throws IOException { - reader.consumeWhiteSpace(); - if (reader.peekFor('"')) { - return Base64.getDecoder().decode(reader.readString()); - } - return decodeExtendedBinaryObject(reader); - } - - @Nonnull - @Override - public Schema toSchema(@Nonnull SchemaContext context) { - StringSchema base64 = new StringSchema(); - base64.setPattern(Codec.BASE64_PATTERN); - base64.setTitle("Binary payload"); - return base64; - } - - @Nonnull - private static byte[] decodeExtendedBinaryObject(@Nonnull RawJsonReader reader) throws IOException { - reader.expect('{'); - reader.consumeWhiteSpace(); - - byte[] payload = null; - while (true) { - String key = reader.readString(); - reader.consumeWhiteSpace(); - reader.expect(':'); - reader.consumeWhiteSpace(); - - if ("$binary".equals(key)) { - payload = decodeBinaryValue(reader); - } else { - reader.skipValue(); - } - - reader.consumeWhiteSpace(); - if (reader.tryConsumeOrExpect('}', ',')) { - if (payload == null) { - throw new CodecException("Expected '$binary' field"); - } - return payload; - } - reader.consumeWhiteSpace(); - } - } - - @Nonnull - private static byte[] decodeBinaryValue(@Nonnull RawJsonReader reader) throws IOException { - if (reader.peekFor('"')) { - return Base64.getDecoder().decode(reader.readString()); - } - - reader.expect('{'); - reader.consumeWhiteSpace(); - - String base64 = null; - while (true) { - String key = reader.readString(); - reader.consumeWhiteSpace(); - reader.expect(':'); - reader.consumeWhiteSpace(); - - if ("base64".equals(key)) { - base64 = reader.readString(); - } else { - reader.skipValue(); - } - - reader.consumeWhiteSpace(); - if (reader.tryConsumeOrExpect('}', ',')) { - if (base64 == null) { - throw new CodecException("Expected '$binary.base64' field"); - } - return Base64.getDecoder().decode(base64); - } - reader.consumeWhiteSpace(); - } - } - - @Nonnull - private static byte[] copyPayload(byte[] payload) { - if (payload == null || payload.length == 0) { - return EMPTY_PAYLOAD; - } - return Arrays.copyOf(payload, payload.length); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsBodyState.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsBodyState.java deleted file mode 100644 index 95ca5dbc..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsBodyState.java +++ /dev/null @@ -1,522 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.persistence; - -import com.hypixel.hytale.codec.Codec; -import com.hypixel.hytale.codec.KeyedCodec; -import com.hypixel.hytale.codec.builder.BuilderCodec; -import com.hypixel.hytale.codec.codecs.EnumCodec; -import com.hypixel.hytale.codec.validation.Validators; -import com.hypixel.hytale.math.vector.Vector3fUtil; -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; -import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistration; -import dev.hytalemodding.impulse.core.plugin.codec.ImpulseCodecs; -import java.util.Objects; -import java.util.UUID; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import lombok.Getter; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -/** - * World-level persistent state for one physics body. - */ -public class PersistentPhysicsBodyState { - - @Nonnull - public static final BuilderCodec CODEC = BuilderCodec.builder( - PersistentPhysicsBodyState.class, - PersistentPhysicsBodyState::new) - .append(new KeyedCodec<>("BodyId", Codec.UUID_BINARY), - (state, value) -> state.bodyId = value, - PersistentPhysicsBodyState::getBodyIdValue) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("SpaceId", Codec.INTEGER), - (state, value) -> state.spaceId = value, - PersistentPhysicsBodyState::getSpaceId) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, Integer.MAX_VALUE)) - .add() - .append(new KeyedCodec<>("ShapeType", new EnumCodec<>(ShapeType.class)), - (state, value) -> state.shapeType = value, - PersistentPhysicsBodyState::getShapeType) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.persistentShapeType()) - .add() - .append(new KeyedCodec<>("ShapeAxis", new EnumCodec<>(PhysicsAxis.class)), - (state, value) -> state.shapeAxis = value, - PersistentPhysicsBodyState::getShapeAxis) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("BoxHalfExtents", Vector3fUtil.CODEC), - (state, value) -> state.boxHalfExtents.set(value), - PersistentPhysicsBodyState::getBoxHalfExtents) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.finiteVector( - "Persisted body box half extents must be finite")) - .add() - .append(new KeyedCodec<>("SphereRadius", Codec.FLOAT), - (state, value) -> state.sphereRadius = value, - PersistentPhysicsBodyState::getSphereRadius) - .addValidator(PersistentPhysicsValidation.nonNegativeFiniteFloat( - "Persisted body sphere radius must be finite and >= 0")) - .add() - .append(new KeyedCodec<>("HalfHeight", Codec.FLOAT), - (state, value) -> state.halfHeight = value, - PersistentPhysicsBodyState::getHalfHeight) - .addValidator(PersistentPhysicsValidation.nonNegativeFiniteFloat( - "Persisted body half height must be finite and >= 0")) - .add() - .append(new KeyedCodec<>("BodyType", new EnumCodec<>(PhysicsBodyType.class)), - (state, value) -> state.bodyType = value, - PersistentPhysicsBodyState::getBodyType) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("Mass", Codec.FLOAT), - (state, value) -> state.mass = value, - PersistentPhysicsBodyState::getMass) - .addValidator(PersistentPhysicsValidation.nonNegativeFiniteFloat( - "Persisted body mass must be finite and >= 0")) - .add() - .append(new KeyedCodec<>("Position", Vector3fUtil.CODEC), - (state, value) -> state.position.set(value), - PersistentPhysicsBodyState::getPosition) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.finiteVector( - "Persisted body position must be finite")) - .add() - .append(new KeyedCodec<>("Rotation", ImpulseCodecs.QUATERNIONF), - (state, value) -> { - if (value != null) { - copyNormalizedQuaternionIfValid(state.rotation, value); - } - }, - PersistentPhysicsBodyState::getRotation) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("LinearVelocity", Vector3fUtil.CODEC), - (state, value) -> state.linearVelocity.set(value), - PersistentPhysicsBodyState::getLinearVelocity) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.finiteVector( - "Persisted body linear velocity must be finite")) - .add() - .append(new KeyedCodec<>("AngularVelocity", Vector3fUtil.CODEC), - (state, value) -> state.angularVelocity.set(value), - PersistentPhysicsBodyState::getAngularVelocity) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.finiteVector( - "Persisted body angular velocity must be finite")) - .add() - .append(new KeyedCodec<>("Friction", Codec.FLOAT), - (state, value) -> state.friction = value, - PersistentPhysicsBodyState::getFriction) - .addValidator(PersistentPhysicsValidation.nonNegativeFiniteFloat( - "Persisted body friction must be finite and >= 0")) - .add() - .append(new KeyedCodec<>("Restitution", Codec.FLOAT), - (state, value) -> state.restitution = value, - PersistentPhysicsBodyState::getRestitution) - .addValidator(PersistentPhysicsValidation.nonNegativeFiniteFloat( - "Persisted body restitution must be finite and >= 0")) - .add() - .append(new KeyedCodec<>("LinearDamping", Codec.FLOAT), - (state, value) -> state.linearDamping = value, - PersistentPhysicsBodyState::getLinearDamping) - .addValidator(PersistentPhysicsValidation.nonNegativeFiniteFloat( - "Persisted body linear damping must be finite and >= 0")) - .add() - .append(new KeyedCodec<>("AngularDamping", Codec.FLOAT), - (state, value) -> state.angularDamping = value, - PersistentPhysicsBodyState::getAngularDamping) - .addValidator(PersistentPhysicsValidation.nonNegativeFiniteFloat( - "Persisted body angular damping must be finite and >= 0")) - .add() - .append(new KeyedCodec<>("Sensor", Codec.BOOLEAN), - (state, value) -> state.sensor = value, - PersistentPhysicsBodyState::isSensor) - .add() - .append(new KeyedCodec<>("CollisionGroup", Codec.INTEGER), - (state, value) -> state.collisionGroup = value, - PersistentPhysicsBodyState::getCollisionGroup) - .add() - .append(new KeyedCodec<>("CollisionMask", Codec.INTEGER), - (state, value) -> state.collisionMask = value, - PersistentPhysicsBodyState::getCollisionMask) - .add() - .append(new KeyedCodec<>("ContinuousCollision", Codec.BOOLEAN), - (state, value) -> state.continuousCollisionEnabled = value, - PersistentPhysicsBodyState::isContinuousCollisionEnabled) - .add() - .append(new KeyedCodec<>("Sleeping", Codec.BOOLEAN), - (state, value) -> state.sleeping = value, - PersistentPhysicsBodyState::isSleeping) - .add() - .build(); - - @Nullable - private UUID bodyId; - @Getter - private int spaceId; - @Nonnull - private ShapeType shapeType = ShapeType.UNKNOWN; - @Nonnull - private PhysicsAxis shapeAxis = PhysicsAxis.Y; - @Nonnull - private final Vector3f boxHalfExtents = new Vector3f(); - @Getter - private float sphereRadius; - @Getter - private float halfHeight; - @Nonnull - private PhysicsBodyType bodyType = PhysicsBodyType.DYNAMIC; - @Getter - private float mass = 1.0f; - @Nonnull - private final Vector3f position = new Vector3f(); - @Nonnull - private final Quaternionf rotation = new Quaternionf(); - @Nonnull - private final Vector3f linearVelocity = new Vector3f(); - @Nonnull - private final Vector3f angularVelocity = new Vector3f(); - @Getter - private float friction; - @Getter - private float restitution; - @Getter - private float linearDamping; - @Getter - private float angularDamping; - @Getter - private boolean sensor; - @Getter - private int collisionGroup; - @Getter - private int collisionMask; - @Getter - private boolean continuousCollisionEnabled; - @Getter - private boolean sleeping; - - public PersistentPhysicsBodyState() { - } - - @Nonnull - public static PersistentPhysicsBodyState from(@Nonnull PhysicsBodyRegistration registration, - @Nonnull PhysicsBodySnapshot snapshot) { - PersistentPhysicsBodyState state = new PersistentPhysicsBodyState(); - state.bodyId = registration.bodyKey().value(); - state.updateFromSnapshot(snapshot, registration.spaceId()); - return state; - } - - @Nullable - public RigidBodyKey getBodyKey() { - return bodyId != null ? RigidBodyKey.of(bodyId) : null; - } - - @Nullable - public UUID getBodyIdValue() { - return bodyId; - } - - @Nonnull - public ShapeType getShapeType() { - return shapeType; - } - - @Nonnull - public PhysicsAxis getShapeAxis() { - return shapeAxis; - } - - @Nonnull - public Vector3f getBoxHalfExtents() { - return boxHalfExtents; - } - - @Nonnull - public PhysicsBodyType getBodyType() { - return bodyType; - } - - @Nonnull - public Vector3f getPosition() { - return position; - } - - @Nonnull - public Quaternionf getRotation() { - return new Quaternionf(rotation); - } - - @Nonnull - public Vector3f getLinearVelocity() { - return linearVelocity; - } - - @Nonnull - public Vector3f getAngularVelocity() { - return angularVelocity; - } - - public int resolveSpaceId() { - return spaceId; - } - - @Nullable - public String restoreValidationFailureReason() { - if (bodyId == null) { - return "missing body key"; - } - if (shapeType == ShapeType.UNKNOWN || shapeType == ShapeType.VOXELS) { - return "unsupported body shape"; - } - if (!isFiniteVector(position)) { - return "invalid position"; - } - if (!ImpulseCodecs.isFiniteAndNonZero(rotation)) { - return "invalid rotation"; - } - if (!isFiniteVector(linearVelocity)) { - return "invalid linear velocity"; - } - if (!isFiniteVector(angularVelocity)) { - return "invalid angular velocity"; - } - if (!isNonNegativeFiniteForRestore(mass)) { - return "invalid mass"; - } - if (bodyType == PhysicsBodyType.DYNAMIC && mass <= 0.0f) { - return "invalid dynamic mass"; - } - if (!isNonNegativeFiniteForRestore(friction)) { - return "invalid friction"; - } - if (!isNonNegativeFiniteForRestore(restitution)) { - return "invalid restitution"; - } - if (!isNonNegativeFiniteForRestore(linearDamping)) { - return "invalid linear damping"; - } - if (!isNonNegativeFiniteForRestore(angularDamping)) { - return "invalid angular damping"; - } - if (shapeType == ShapeType.BOX && !isPositiveFiniteVector(boxHalfExtents)) { - return "invalid box half extents"; - } - if (shapeType == ShapeType.SPHERE && !isPositiveFiniteForRestore(sphereRadius)) { - return "invalid sphere radius"; - } - if (usesRadiusAndHalfHeight(shapeType) - && (!isPositiveFiniteForRestore(sphereRadius) || !isPositiveFiniteForRestore(halfHeight))) { - return "invalid swept shape dimensions"; - } - return null; - } - - public void updateFromSnapshot(@Nonnull PhysicsBodySnapshot snapshot, @Nonnull SpaceId spaceId) { - Objects.requireNonNull(spaceId, "spaceId"); - if (spaceId.value() <= 0) { - throw new IllegalArgumentException( - "Persistent body state requires a positive explicit space id"); - } - this.spaceId = spaceId.value(); - shapeType = snapshot.shapeType(); - shapeAxis = snapshot.shapeAxis(); - Vector3f halfExtents = snapshot.boxHalfExtents(); - if (isFiniteVector(halfExtents)) { - boxHalfExtents.set(halfExtents); - } else { - boxHalfExtents.zero(); - } - sphereRadius = positiveFiniteOrZeroForRestore(snapshot.sphereRadius()); - halfHeight = positiveFiniteOrZeroForRestore(snapshot.halfHeight()); - bodyType = snapshot.bodyType(); - mass = nonNegativeFiniteOrDefaultForSnapshot(snapshot.mass(), bodyType == PhysicsBodyType.DYNAMIC ? 1.0f : 0.0f); - copyFiniteVectorOrZero(position, snapshot.position()); - copyNormalizedQuaternionIfValid(rotation, snapshot.rotation()); - copyFiniteVectorOrZero(linearVelocity, snapshot.linearVelocity()); - copyFiniteVectorOrZero(angularVelocity, snapshot.angularVelocity()); - friction = nonNegativeFiniteOrZeroForRestore(snapshot.friction()); - restitution = nonNegativeFiniteOrZeroForRestore(snapshot.restitution()); - linearDamping = nonNegativeFiniteOrZeroForRestore(snapshot.linearDamping()); - angularDamping = nonNegativeFiniteOrZeroForRestore(snapshot.angularDamping()); - sensor = snapshot.sensor(); - collisionGroup = snapshot.collisionGroup(); - collisionMask = snapshot.collisionMask(); - continuousCollisionEnabled = snapshot.continuousCollisionEnabled(); - sleeping = snapshot.sleeping(); - } - - @Nonnull - public BackendBodyHandle createBackendBody(@Nonnull PhysicsSpaceBinding space) { - float dynamicMass = bodyType == PhysicsBodyType.DYNAMIC ? mass : 0.0f; - Quaternionf restoreRotation = normalizedRotationForRestore(); - long backendBodyId = switch (shapeType) { - case BOX, SPHERE, CAPSULE, CYLINDER, CONE, PLANE -> space.runtime().createBody( - space.backendSpaceHandle().value(), - BackendRuntimeCodes.shapeTypeCode(shapeType), - boxHalfExtents.x, - boxHalfExtents.y, - boxHalfExtents.z, - sphereRadius, - halfHeight, - BackendRuntimeCodes.axisCode(shapeAxis), - finiteOrZero(position.y), - dynamicMass, - BackendRuntimeCodes.bodyTypeCode(bodyType), - position.x, - position.y, - position.z, - restoreRotation.x, - restoreRotation.y, - restoreRotation.z, - restoreRotation.w); - case VOXELS -> throw new IllegalStateException( - "PersistentPhysicsBodyState cannot rebuild streamed voxel terrain bodies"); - case UNKNOWN -> throw new IllegalStateException("Persistent body shape is unknown"); - }; - return new BackendBodyHandle(backendBodyId); - } - - public void applyToBody(@Nonnull PhysicsSpaceBinding space, @Nonnull BackendBodyHandle backendBodyHandle) { - long backendBodyId = backendBodyHandle.value(); - Quaternionf restoreRotation = normalizedRotationForRestore(); - space.runtime().setBodyType(space.backendSpaceHandle().value(), - backendBodyId, - BackendRuntimeCodes.bodyTypeCode(bodyType)); - space.runtime().setBodyTransform(space.backendSpaceHandle().value(), - backendBodyId, - position.x, - position.y, - position.z, - restoreRotation.x, - restoreRotation.y, - restoreRotation.z, - restoreRotation.w); - space.runtime().setBodyVelocity(space.backendSpaceHandle().value(), - backendBodyId, - linearVelocity.x, - linearVelocity.y, - linearVelocity.z, - angularVelocity.x, - angularVelocity.y, - angularVelocity.z); - space.runtime().setBodyFriction(space.backendSpaceHandle().value(), backendBodyId, friction); - space.runtime().setBodyRestitution(space.backendSpaceHandle().value(), backendBodyId, restitution); - space.runtime().setBodyDamping(space.backendSpaceHandle().value(), backendBodyId, linearDamping, angularDamping); - space.runtime().setBodySensor(space.backendSpaceHandle().value(), backendBodyId, sensor); - space.runtime().setBodyCollisionFilter(space.backendSpaceHandle().value(), backendBodyId, collisionGroup, collisionMask); - space.runtime() - .setBodyContinuousCollision(space.backendSpaceHandle().value(), backendBodyId, continuousCollisionEnabled); - if (sleeping && bodyType == PhysicsBodyType.DYNAMIC) { - space.runtime().sleepBody(space.backendSpaceHandle().value(), backendBodyId); - } else { - space.runtime().activateBody(space.backendSpaceHandle().value(), backendBodyId); - } - } - - @Nonnull - public PersistentPhysicsBodyState copy() { - PersistentPhysicsBodyState copy = new PersistentPhysicsBodyState(); - copy.bodyId = bodyId; - copy.spaceId = spaceId; - copy.shapeType = shapeType; - copy.shapeAxis = shapeAxis; - copy.boxHalfExtents.set(boxHalfExtents); - copy.sphereRadius = sphereRadius; - copy.halfHeight = halfHeight; - copy.bodyType = bodyType; - copy.mass = mass; - copy.position.set(position); - copy.rotation.set(rotation); - copy.linearVelocity.set(linearVelocity); - copy.angularVelocity.set(angularVelocity); - copy.friction = friction; - copy.restitution = restitution; - copy.linearDamping = linearDamping; - copy.angularDamping = angularDamping; - copy.sensor = sensor; - copy.collisionGroup = collisionGroup; - copy.collisionMask = collisionMask; - copy.continuousCollisionEnabled = continuousCollisionEnabled; - copy.sleeping = sleeping; - return copy; - } - - private static void copyFiniteVectorOrZero(@Nonnull Vector3f target, @Nullable Vector3f value) { - if (isFiniteVector(value)) { - target.set(value); - } else { - target.zero(); - } - } - - private static void copyNormalizedQuaternionIfValid(@Nonnull Quaternionf target, - @Nonnull Quaternionf value) { - target.set(value); - if (ImpulseCodecs.isFiniteAndNonZero(target)) { - target.normalize(); - } - } - - @Nonnull - private Quaternionf normalizedRotationForRestore() { - Quaternionf restoreRotation = new Quaternionf(rotation); - if (ImpulseCodecs.isFiniteAndNonZero(restoreRotation)) { - restoreRotation.normalize(); - } - return restoreRotation; - } - - private static boolean isFiniteVector(@Nullable Vector3f value) { - return value != null - && Float.isFinite(value.x) - && Float.isFinite(value.y) - && Float.isFinite(value.z); - } - - private static boolean isPositiveFiniteVector(@Nonnull Vector3f value) { - return isPositiveFiniteForRestore(value.x) - && isPositiveFiniteForRestore(value.y) - && isPositiveFiniteForRestore(value.z); - } - - private static float positiveFiniteOrZeroForRestore(float value) { - return isPositiveFiniteForRestore(value) ? value : 0.0f; - } - - private static boolean isPositiveFiniteForRestore(float value) { - return Float.isFinite(value) && value > 0.0f; - } - - private static float nonNegativeFiniteOrZeroForRestore(float value) { - return isNonNegativeFiniteForRestore(value) ? value : 0.0f; - } - - private static float nonNegativeFiniteOrDefaultForSnapshot(float value, float defaultValue) { - return isNonNegativeFiniteForRestore(value) ? value : defaultValue; - } - - private static boolean isNonNegativeFiniteForRestore(float value) { - return Float.isFinite(value) && value >= 0.0f; - } - - private static boolean usesRadiusAndHalfHeight(@Nullable ShapeType type) { - return type == ShapeType.CAPSULE || type == ShapeType.CYLINDER || type == ShapeType.CONE; - } - - private static float finiteOrZero(float value) { - return Float.isFinite(value) ? value : 0.0f; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsExtensionSettingState.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsExtensionSettingState.java deleted file mode 100644 index 97de6edb..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsExtensionSettingState.java +++ /dev/null @@ -1,93 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.persistence; - -import com.hypixel.hytale.codec.Codec; -import com.hypixel.hytale.codec.KeyedCodec; -import com.hypixel.hytale.codec.builder.BuilderCodec; -import com.hypixel.hytale.codec.codecs.EnumCodec; -import com.hypixel.hytale.codec.validation.Validators; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsBackendExtensionId; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsExtensionSettingValue; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsExtensionSettings; -import javax.annotation.Nonnull; -import lombok.Getter; -import lombok.Setter; - -/** - * Persisted capability-keyed extension setting. - */ -@Getter -final class PersistentPhysicsExtensionSettingState { - - @Nonnull - static final BuilderCodec CODEC = - BuilderCodec.builder(PersistentPhysicsExtensionSettingState.class, - PersistentPhysicsExtensionSettingState::new) - .append(new KeyedCodec<>("CapabilityId", Codec.STRING), - (state, value) -> state.capabilityId = value, - PersistentPhysicsExtensionSettingState::getCapabilityId) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.nonBlankString( - "Persisted extension capability id cannot be blank")) - .add() - .append(new KeyedCodec<>("Key", Codec.STRING), - (state, value) -> state.key = value, - PersistentPhysicsExtensionSettingState::getKey) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.nonBlankString( - "Persisted extension setting key cannot be blank")) - .add() - .append(new KeyedCodec<>("Kind", new EnumCodec<>(PhysicsExtensionSettingValue.Kind.class)), - (state, value) -> state.kind = value, - PersistentPhysicsExtensionSettingState::getKind) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("Value", Codec.STRING), - (state, value) -> state.value = value, - PersistentPhysicsExtensionSettingState::getValue) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.nonBlankString( - "Persisted extension setting value cannot be blank")) - .add() - .build(); - - @Nonnull - @Setter - private String capabilityId = ""; - @Nonnull - @Setter - private String key = ""; - @Nonnull - @Setter - private PhysicsExtensionSettingValue.Kind kind = PhysicsExtensionSettingValue.Kind.STRING; - @Nonnull - @Setter - private String value = ""; - - @Nonnull - static PersistentPhysicsExtensionSettingState from(@Nonnull PhysicsBackendExtensionId extensionId, - @Nonnull String key, - @Nonnull PhysicsExtensionSettingValue value) { - PersistentPhysicsExtensionSettingState state = new PersistentPhysicsExtensionSettingState(); - state.capabilityId = extensionId.value(); - state.key = key; - state.kind = value.kind(); - state.value = value.value(); - return state; - } - - void applyTo(@Nonnull PhysicsExtensionSettings settings) { - settings.set(new PhysicsBackendExtensionId(capabilityId), - key, - new PhysicsExtensionSettingValue(kind, value)); - } - - @Nonnull - PersistentPhysicsExtensionSettingState copy() { - PersistentPhysicsExtensionSettingState copy = new PersistentPhysicsExtensionSettingState(); - copy.capabilityId = capabilityId; - copy.key = key; - copy.kind = kind; - copy.value = value; - return copy; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsJointState.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsJointState.java deleted file mode 100644 index 3b651ec2..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsJointState.java +++ /dev/null @@ -1,357 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.persistence; - -import com.hypixel.hytale.codec.Codec; -import com.hypixel.hytale.codec.ExtraInfo; -import com.hypixel.hytale.codec.KeyedCodec; -import com.hypixel.hytale.codec.builder.BuilderCodec; -import com.hypixel.hytale.codec.codecs.EnumCodec; -import com.hypixel.hytale.codec.validation.ValidationResults; -import com.hypixel.hytale.codec.validation.Validators; -import com.hypixel.hytale.math.vector.Vector3fUtil; -import dev.hytalemodding.impulse.api.PhysicsJointType; -import dev.hytalemodding.impulse.core.internal.resources.joint.PhysicsJointRegistration; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.JointType; -import java.util.UUID; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import lombok.Getter; -import lombok.Setter; -import org.joml.Vector3f; - -/** - * Codec-backed definition of one physics joint for the persistence layer. - *

- * This class uses stable physics body keys to identify the two endpoint bodies. - * - *

The {@link #key()} method produces a deterministic string from all fields - * (using {@link Float#floatToIntBits(float)} to avoid floating-point drift). - * The joint hydration system uses this to detect whether a given joint already - * exists in the runtime space and avoid duplicating it on repeated ticks.

- */ -public class PersistentPhysicsJointState { - - @Nonnull - public static final BuilderCodec CODEC = BuilderCodec.builder( - PersistentPhysicsJointState.class, - PersistentPhysicsJointState::new) - .append(new KeyedCodec<>("SpaceId", Codec.INTEGER), - (state, value) -> state.spaceId = value, - PersistentPhysicsJointState::getSpaceId) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, Integer.MAX_VALUE)) - .add() - .append(new KeyedCodec<>("BodyAId", Codec.UUID_BINARY), - (state, value) -> state.bodyAId = value, - PersistentPhysicsJointState::getBodyAIdValue) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("BodyBId", Codec.UUID_BINARY), - (state, value) -> state.bodyBId = value, - PersistentPhysicsJointState::getBodyBIdValue) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("Type", new EnumCodec<>(PhysicsJointType.class)), - (state, value) -> state.type = value, - PersistentPhysicsJointState::getType) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("AnchorA", Vector3fUtil.CODEC), - (state, value) -> state.anchorA.set(value), - PersistentPhysicsJointState::getAnchorA) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.finiteVector( - "Persisted joint anchor A must be finite")) - .add() - .append(new KeyedCodec<>("AnchorB", Vector3fUtil.CODEC), - (state, value) -> state.anchorB.set(value), - PersistentPhysicsJointState::getAnchorB) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.finiteVector( - "Persisted joint anchor B must be finite")) - .add() - .append(new KeyedCodec<>("Axis", Vector3fUtil.CODEC, true), - (state, value) -> state.axis = value != null ? new Vector3f(value) : null, - PersistentPhysicsJointState::getAxis) - .addValidator(PersistentPhysicsValidation.finiteVector( - "Persisted joint axis must be finite")) - .add() - .append(new KeyedCodec<>("LowerLimit", Codec.FLOAT), - (state, value) -> state.lowerLimit = value, - PersistentPhysicsJointState::getLowerLimit) - .addValidator(PersistentPhysicsValidation.finiteFloat( - "Persisted joint lower limit must be finite")) - .add() - .append(new KeyedCodec<>("UpperLimit", Codec.FLOAT), - (state, value) -> state.upperLimit = value, - PersistentPhysicsJointState::getUpperLimit) - .addValidator(PersistentPhysicsValidation.finiteFloat( - "Persisted joint upper limit must be finite")) - .add() - .append(new KeyedCodec<>("Enabled", Codec.BOOLEAN), - (state, value) -> state.enabled = value, - PersistentPhysicsJointState::isEnabled) - .add() - .append(new KeyedCodec<>("MotorEnabled", Codec.BOOLEAN), - (state, value) -> state.motorEnabled = value, - PersistentPhysicsJointState::isMotorEnabled) - .add() - .append(new KeyedCodec<>("MotorTargetVelocity", Codec.FLOAT), - (state, value) -> state.motorTargetVelocity = value, - PersistentPhysicsJointState::getMotorTargetVelocity) - .addValidator(PersistentPhysicsValidation.finiteFloat( - "Persisted joint motor target velocity must be finite")) - .add() - .append(new KeyedCodec<>("MotorMaxForce", Codec.FLOAT), - (state, value) -> state.motorMaxForce = value, - PersistentPhysicsJointState::getMotorMaxForce) - .addValidator(PersistentPhysicsValidation.nonNegativeFiniteFloat( - "Persisted joint motor max force must be finite and >= 0")) - .add() - .append(new KeyedCodec<>("SpringRestLength", Codec.FLOAT, false), - (state, value) -> state.springRestLength = value, - PersistentPhysicsJointState::getSpringRestLength) - .addValidator(PersistentPhysicsValidation.nonNegativeFiniteFloat( - "Persisted joint spring rest length must be finite and >= 0")) - .add() - .append(new KeyedCodec<>("SpringStiffness", Codec.FLOAT, false), - (state, value) -> state.springStiffness = value, - PersistentPhysicsJointState::getSpringStiffness) - .addValidator(PersistentPhysicsValidation.nonNegativeFiniteFloat( - "Persisted joint spring stiffness must be finite and >= 0")) - .add() - .append(new KeyedCodec<>("SpringDamping", Codec.FLOAT, false), - (state, value) -> state.springDamping = value, - PersistentPhysicsJointState::getSpringDamping) - .addValidator(PersistentPhysicsValidation.nonNegativeFiniteFloat( - "Persisted joint spring damping must be finite and >= 0")) - .add() - .afterDecode(PersistentPhysicsJointState::validateAfterDecode) - .build(); - - @Getter - @Setter - private int spaceId; - @Nullable - private UUID bodyAId; - @Nullable - private UUID bodyBId; - @Nonnull - @Setter - private PhysicsJointType type = PhysicsJointType.FIXED; - @Nonnull - private final Vector3f anchorA = new Vector3f(); - @Nonnull - private final Vector3f anchorB = new Vector3f(); - @Nullable - private Vector3f axis; - @Getter - @Setter - private float lowerLimit; - @Getter - @Setter - private float upperLimit; - @Getter - @Setter - private boolean enabled = true; - @Getter - @Setter - private boolean motorEnabled; - @Getter - @Setter - private float motorTargetVelocity; - @Getter - @Setter - private float motorMaxForce; - @Getter - @Setter - private float springRestLength = 0f; - @Getter - @Setter - private float springStiffness = 0f; - @Getter - @Setter - private float springDamping = 0f; - - public PersistentPhysicsJointState() { - } - - @Nonnull - public PhysicsJointType getType() { - return type; - } - - @Nonnull - public Vector3f getAnchorA() { - return anchorA; - } - - @Nonnull - public Vector3f getAnchorB() { - return anchorB; - } - - @Nullable - public Vector3f getAxis() { - return axis; - } - - @Nonnull - public static PersistentPhysicsJointState from(int spaceId, - @Nonnull RigidBodyKey bodyAKey, - @Nonnull RigidBodyKey bodyBKey, - @Nonnull PhysicsJointRegistration joint) { - PersistentPhysicsJointState state = new PersistentPhysicsJointState(); - state.spaceId = spaceId; - state.bodyAId = bodyAKey.value(); - state.bodyBId = bodyBKey.value(); - state.type = toPersistentJointType(joint.type()); - state.anchorA.set(joint.anchorAX(), joint.anchorAY(), joint.anchorAZ()); - state.anchorB.set(joint.anchorBX(), joint.anchorBY(), joint.anchorBZ()); - Vector3f jointAxis = new Vector3f(joint.axisX(), joint.axisY(), joint.axisZ()); - state.axis = jointAxis.lengthSquared() > 0.0f ? jointAxis : null; - state.lowerLimit = joint.lowerLimit(); - state.upperLimit = joint.upperLimit(); - state.enabled = true; - state.motorEnabled = joint.motorEnabled(); - state.motorTargetVelocity = joint.motorTargetVelocity(); - state.motorMaxForce = joint.motorMaxForce(); - state.springRestLength = nanToZero(joint.restLength()); - state.springStiffness = nanToZero(joint.stiffness()); - state.springDamping = nanToZero(joint.damping()); - return state; - } - - @Nullable - public RigidBodyKey getBodyAKey() { - return bodyAId != null ? RigidBodyKey.of(bodyAId) : null; - } - - @Nullable - public UUID getBodyAIdValue() { - return bodyAId; - } - - public void setBodyAKey(@Nullable RigidBodyKey bodyAKey) { - this.bodyAId = bodyAKey != null ? bodyAKey.value() : null; - } - - @Nullable - public RigidBodyKey getBodyBKey() { - return bodyBId != null ? RigidBodyKey.of(bodyBId) : null; - } - - @Nullable - public UUID getBodyBIdValue() { - return bodyBId; - } - - public void setBodyBKey(@Nullable RigidBodyKey bodyBKey) { - this.bodyBId = bodyBKey != null ? bodyBKey.value() : null; - } - - public void setAxis(@Nullable Vector3f axis) { - this.axis = axis != null ? new Vector3f(axis) : null; - } - - @Nonnull - public String key() { - return String.valueOf(spaceId) - + '|' - + type.name() - + '|' - + bodyAId - + '|' - + bodyBId - + '|' - + bits(anchorA.x) - + '|' - + bits(anchorA.y) - + '|' - + bits(anchorA.z) - + '|' - + bits(anchorB.x) - + '|' - + bits(anchorB.y) - + '|' - + bits(anchorB.z) - + '|' - + (axis != null ? bits(axis.x) : "na") - + '|' - + (axis != null ? bits(axis.y) : "na") - + '|' - + (axis != null ? bits(axis.z) : "na") - + '|' - + bits(lowerLimit) - + '|' - + bits(upperLimit) - + '|' - + enabled - + '|' - + motorEnabled - + '|' - + bits(motorTargetVelocity) - + '|' - + bits(motorMaxForce) - + '|' - + bits(springRestLength) - + '|' - + bits(springStiffness) - + '|' - + bits(springDamping); - } - - private static int bits(float value) { - return Float.floatToIntBits(value); - } - - private static float nanToZero(float value) { - return Float.isNaN(value) ? 0f : value; - } - - @Nonnull - private static PhysicsJointType toPersistentJointType(@Nonnull JointType type) { - return switch (type) { - case FIXED -> PhysicsJointType.FIXED; - case POINT -> PhysicsJointType.POINT; - case HINGE -> PhysicsJointType.HINGE; - case SLIDER -> PhysicsJointType.SLIDER; - case SPRING -> PhysicsJointType.SPRING; - }; - } - - @Nonnull - public PersistentPhysicsJointState copy() { - PersistentPhysicsJointState copy = new PersistentPhysicsJointState(); - copy.spaceId = spaceId; - copy.bodyAId = bodyAId; - copy.bodyBId = bodyBId; - copy.type = type; - copy.anchorA.set(anchorA); - copy.anchorB.set(anchorB); - copy.axis = axis != null ? new Vector3f(axis) : null; - copy.lowerLimit = lowerLimit; - copy.upperLimit = upperLimit; - copy.enabled = enabled; - copy.motorEnabled = motorEnabled; - copy.motorTargetVelocity = motorTargetVelocity; - copy.motorMaxForce = motorMaxForce; - copy.springRestLength = springRestLength; - copy.springStiffness = springStiffness; - copy.springDamping = springDamping; - return copy; - } - - private static void validateAfterDecode(@Nonnull PersistentPhysicsJointState state, - @Nonnull ExtraInfo extraInfo) { - ValidationResults results = extraInfo.getValidationResults(); - if ((state.type == PhysicsJointType.HINGE || state.type == PhysicsJointType.SLIDER) - && state.axis == null) { - results.fail("Persisted " + state.type + " joint requires an axis"); - } - if (state.lowerLimit > state.upperLimit) { - results.fail("Persisted joint lower limit cannot exceed upper limit"); - } - results._processValidationResults(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsRestorePreflight.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsRestorePreflight.java deleted file mode 100644 index fe3a522f..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsRestorePreflight.java +++ /dev/null @@ -1,173 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.persistence; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import it.unimi.dsi.fastutil.ints.IntOpenHashSet; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3f; - -/** - * Validates hard restore invariants before live runtime topology is stripped. - */ -public final class PersistentPhysicsRestorePreflight { - - private PersistentPhysicsRestorePreflight() { - } - - @Nullable - public static String validate(@Nonnull PersistentPhysicsWorldResource persistent) { - PhysicsWorldSettings worldSettings = persistent.getWorldSettings(); - String worldSettingsFailure = validateWorldSettings(worldSettings); - if (worldSettingsFailure != null) { - return worldSettingsFailure; - } - - String spaceFailure = validateSpaces(persistent.getSpaces(), worldSettings.getStepMode()); - if (spaceFailure != null) { - return spaceFailure; - } - - String bodyFailure = validateBodyKeys(persistent.getBodies()); - if (bodyFailure != null) { - return bodyFailure; - } - - return validateJointKeys(persistent.getJoints()); - } - - @Nullable - private static String validateWorldSettings(@Nonnull PhysicsWorldSettings settings) { - try { - PhysicsWorldSettings validated = PhysicsWorldSettings.defaults(); - validated.setSimulationSteps(settings.getSimulationSteps()); - validated.setStepMode(settings.getStepMode()); - validated.setStepSchedulingMode(settings.getStepSchedulingMode()); - validated.setEventCollectionMode(settings.getEventCollectionMode()); - validated.setMaxStepDt(settings.getMaxStepDt()); - return null; - } catch (RuntimeException exception) { - return "Invalid persisted physics runtime settings: " + exception.getMessage(); - } - } - - @Nullable - private static String validateSpaces(@Nonnull PersistentPhysicsSpaceState[] spaces, - @Nonnull PhysicsStepMode stepMode) { - IntOpenHashSet spaceIds = new IntOpenHashSet(); - for (PersistentPhysicsSpaceState state : spaces) { - int spaceId = state.getSpaceId(); - if (spaceId <= 0) { - return "Persisted space id must be positive, found " + spaceId; - } - if (!spaceIds.add(spaceId)) { - return "Duplicate persisted space id " + spaceId; - } - if (!isFiniteVector(state.getGravity())) { - return "Persisted space gravity must be finite for space id=" + spaceId; - } - - String settingsFailure = validateSpaceBackendAndSettings(state, spaceId, stepMode); - if (settingsFailure != null) { - return settingsFailure; - } - } - return null; - } - - @Nullable - private static String validateSpaceBackendAndSettings(@Nonnull PersistentPhysicsSpaceState state, - int spaceId, - @Nonnull PhysicsStepMode stepMode) { - BackendId backendId; - try { - /* - * BackendId construction validates the serialized id. Keep that check inside - * preflight so bootstrap records a restore failure instead of throwing mid-tick. - */ - backendId = state.toBackendId(); - } catch (RuntimeException exception) { - return "Invalid persisted physics space backend id for space id=" + spaceId - + ": " + failureMessage(exception); - } - - try { - Impulse.getRuntimeProvider(backendId); - } catch (IllegalStateException exception) { - return "Saved backend " + backendId + " is not available for restore"; - } - - try { - state.toSettings(); - validateBackendStepModeCompatibility(backendId, stepMode); - } catch (RuntimeException exception) { - return "Invalid persisted physics space settings for space id=" + spaceId - + " backend=" + backendId + ": " + failureMessage(exception); - } - return null; - } - - private static void validateBackendStepModeCompatibility(@Nonnull BackendId backendId, - @Nonnull PhysicsStepMode stepMode) { - if (stepMode != PhysicsStepMode.CCD) { - return; - } - - PhysicsBackendRuntime probe = Impulse.createRuntime(backendId); - SpaceId probeSpaceId = SpaceId.next(); - int backendSpaceId = probe.createSpace(probeSpaceId); - try { - if (!probe.supportsContinuousCollision(backendSpaceId)) { - throw new IllegalArgumentException("CCD mode is not available for backend " - + backendId); - } - } finally { - probe.destroySpace(backendSpaceId); - } - } - - @Nullable - private static String validateBodyKeys(@Nonnull PersistentPhysicsBodyState[] bodies) { - ObjectOpenHashSet bodyKeys = new ObjectOpenHashSet<>(); - for (PersistentPhysicsBodyState state : bodies) { - RigidBodyKey bodyKey = state.getBodyKey(); - if (bodyKey != null && !bodyKeys.add(bodyKey)) { - return "Duplicate persisted body key " + bodyKey; - } - } - return null; - } - - @Nullable - private static String validateJointKeys(@Nonnull PersistentPhysicsJointState[] joints) { - ObjectOpenHashSet jointKeys = new ObjectOpenHashSet<>(); - for (PersistentPhysicsJointState state : joints) { - String jointKey = state.key(); - if (!jointKeys.add(jointKey)) { - return "Duplicate persisted joint key " + jointKey; - } - } - return null; - } - - private static boolean isFiniteVector(@Nullable Vector3f value) { - return value != null - && Float.isFinite(value.x) - && Float.isFinite(value.y) - && Float.isFinite(value.z); - } - - @Nonnull - private static String failureMessage(@Nonnull RuntimeException exception) { - String message = exception.getMessage(); - return message != null && !message.isBlank() - ? message - : exception.getClass().getSimpleName(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsRuntimeSnapshot.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsRuntimeSnapshot.java deleted file mode 100644 index 0a4888aa..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsRuntimeSnapshot.java +++ /dev/null @@ -1,184 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.persistence; - -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistration; -import dev.hytalemodding.impulse.core.internal.resources.joint.PhysicsJointRegistration; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import javax.annotation.Nonnull; - -/** - * Owner-owned copy of the live runtime state needed by world persistence. - * - *

{@link #capture(PhysicsWorldRuntimeResource)} and - * {@link #captureFootprint(PhysicsWorldRuntimeResource)} - * read live backend spaces, bodies, and joints. Callers must invoke them from the physics - * owner lane or through the physics owner bridge.

- */ -public final class PersistentPhysicsRuntimeSnapshot { - - @Nonnull - private final Footprint footprint; - @Nonnull - private final PhysicsWorldSettings worldSettings; - @Nonnull - private final PersistentPhysicsSpaceState[] spaces; - @Nonnull - private final PersistentPhysicsBodyState[] bodies; - @Nonnull - private final PersistentPhysicsJointState[] joints; - - private PersistentPhysicsRuntimeSnapshot(@Nonnull PhysicsWorldSettings worldSettings, - @Nonnull PersistentPhysicsSpaceState[] spaces, - @Nonnull PersistentPhysicsBodyState[] bodies, - @Nonnull PersistentPhysicsJointState[] joints) { - this.worldSettings = new PhysicsWorldSettings(worldSettings); - this.spaces = copySpaces(spaces); - this.bodies = copyBodies(bodies); - this.joints = copyJoints(joints); - footprint = new Footprint(spaces.length, bodies.length, joints.length); - } - - @Nonnull - public static PersistentPhysicsRuntimeSnapshot capture(@Nonnull PhysicsWorldRuntimeResource runtime) { - runtime.assertCanAccessLiveBackendDirectly("capture persistent physics runtime snapshot"); - - List spaces = new ArrayList<>(); - List bodies = new ArrayList<>(); - List joints = new ArrayList<>(); - for (PhysicsBodyRegistration registration : runtime.getBodyRegistrations()) { - if (registration.persistenceMode() == PhysicsBodyPersistenceMode.PERSISTENT) { - bodies.add(PersistentPhysicsBodyState.from(registration, - runtime.captureLiveBodySnapshot(registration))); - } - } - for (PhysicsSpaceBinding space : runtime.iterateSpaceBindings()) { - PhysicsSpaceSettings settings = runtime.getLiveSpaceSettings(space.spaceId()); - spaces.add(PersistentPhysicsSpaceState.from(space, settings)); - capturePersistentJoints(runtime, space, joints); - } - - return new PersistentPhysicsRuntimeSnapshot(runtime.getWorldSettings(), - spaces.toArray(PersistentPhysicsSpaceState[]::new), - bodies.toArray(PersistentPhysicsBodyState[]::new), - joints.toArray(PersistentPhysicsJointState[]::new)); - } - - @Nonnull - public static Footprint captureFootprint(@Nonnull PhysicsWorldRuntimeResource runtime) { - runtime.assertCanAccessLiveBackendDirectly("capture persistent physics runtime footprint"); - return new Footprint(runtime.getSpaceCount(), - runtime.getBodyRegistrationCount(PhysicsBodyPersistenceMode.PERSISTENT), - countPersistentJoints(runtime)); - } - - @Nonnull - public PhysicsWorldSettings getWorldSettings() { - return new PhysicsWorldSettings(worldSettings); - } - - @Nonnull - public Footprint getFootprint() { - return footprint; - } - - @Nonnull - public PersistentPhysicsSpaceState[] getSpaces() { - return copySpaces(spaces); - } - - @Nonnull - public PersistentPhysicsBodyState[] getBodies() { - return copyBodies(bodies); - } - - @Nonnull - public PersistentPhysicsJointState[] getJoints() { - return copyJoints(joints); - } - - private static int countPersistentJoints(@Nonnull PhysicsWorldRuntimeResource runtime) { - int count = 0; - for (PhysicsJointRegistration joint : runtime.getJointRegistrations()) { - PhysicsBodyRegistration bodyA = runtime.getRegistration(joint.bodyA()); - PhysicsBodyRegistration bodyB = runtime.getRegistration(joint.bodyB()); - if (bodyA == null - || bodyB == null - || bodyA.persistenceMode() != PhysicsBodyPersistenceMode.PERSISTENT - || bodyB.persistenceMode() != PhysicsBodyPersistenceMode.PERSISTENT) { - continue; - } - count++; - } - return count; - } - - private static void capturePersistentJoints(@Nonnull PhysicsWorldRuntimeResource runtime, - @Nonnull PhysicsSpaceBinding space, - @Nonnull List joints) { - for (PhysicsJointRegistration joint : runtime.getJointRegistrations()) { - if (!joint.spaceId().equals(space.spaceId())) { - continue; - } - RigidBodyKey bodyAKey = joint.bodyA(); - RigidBodyKey bodyBKey = joint.bodyB(); - PhysicsBodyRegistration bodyA = runtime.getRegistration(bodyAKey); - PhysicsBodyRegistration bodyB = runtime.getRegistration(bodyBKey); - if (bodyA == null - || bodyB == null - || bodyA.persistenceMode() != PhysicsBodyPersistenceMode.PERSISTENT - || bodyB.persistenceMode() != PhysicsBodyPersistenceMode.PERSISTENT) { - continue; - } - joints.add(PersistentPhysicsJointState.from(space.spaceId().value(), - bodyAKey, - bodyBKey, - joint)); - } - } - - @Nonnull - private static PersistentPhysicsSpaceState[] copySpaces( - @Nonnull PersistentPhysicsSpaceState[] source) { - PersistentPhysicsSpaceState[] copy = Arrays.copyOf(source, source.length); - for (int i = 0; i < copy.length; i++) { - copy[i] = copy[i].copy(); - } - return copy; - } - - @Nonnull - private static PersistentPhysicsBodyState[] copyBodies( - @Nonnull PersistentPhysicsBodyState[] source) { - PersistentPhysicsBodyState[] copy = Arrays.copyOf(source, source.length); - for (int i = 0; i < copy.length; i++) { - copy[i] = copy[i].copy(); - } - return copy; - } - - @Nonnull - private static PersistentPhysicsJointState[] copyJoints( - @Nonnull PersistentPhysicsJointState[] source) { - PersistentPhysicsJointState[] copy = Arrays.copyOf(source, source.length); - for (int i = 0; i < copy.length; i++) { - copy[i] = copy[i].copy(); - } - return copy; - } - - public record Footprint(int spaces, int bodies, int joints) { - - public Footprint { - spaces = Math.max(0, spaces); - bodies = Math.max(0, bodies); - joints = Math.max(0, joints); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsRuntimeSupport.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsRuntimeSupport.java deleted file mode 100644 index 780957a9..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsRuntimeSupport.java +++ /dev/null @@ -1,143 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.persistence; - -import dev.hytalemodding.impulse.api.PhysicsJointType; -import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; -import dev.hytalemodding.impulse.core.internal.resources.BackendJointHandle; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistration; -import dev.hytalemodding.impulse.core.internal.resources.joint.PhysicsJointRegistration; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.simulation.JointType; -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -/** - * Shared helpers for bridging persisted physics state to live runtime objects. - * - *

Used by the hydration and sync systems to construct joints from persisted - * body-key endpoint definitions and produce stable keys for deduplicating joints - * across hydration ticks.

- */ -public final class PersistentPhysicsRuntimeSupport { - - private PersistentPhysicsRuntimeSupport() { - } - - @Nonnull - public static String jointKey(int spaceId, - @Nonnull RigidBodyKey bodyAKey, - @Nonnull RigidBodyKey bodyBKey, - @Nonnull PhysicsJointRegistration joint) { - return PersistentPhysicsJointState.from(spaceId, bodyAKey, bodyBKey, joint).key(); - } - - @Nonnull - public static JointKey createJoint(@Nonnull PhysicsWorldRuntimeResource runtime, - @Nonnull PhysicsSpaceBinding space, - @Nonnull PersistentPhysicsJointState state, - @Nonnull RigidBodyKey bodyAKey, - @Nonnull PhysicsBodyRegistration bodyA, - @Nonnull RigidBodyKey bodyBKey, - @Nonnull PhysicsBodyRegistration bodyB) { - JointType type = toRuntimeJointType(state.getType()); - Vector3f anchorA = new Vector3f(state.getAnchorA()); - Vector3f anchorB = new Vector3f(state.getAnchorB()); - Vector3f axis = state.getType() == PhysicsJointType.HINGE || state.getType() == PhysicsJointType.SLIDER - ? requireAxis(state) - : new Vector3f(); - BackendJointHandle backendJointHandle = new BackendJointHandle(space.runtime().createJoint( - space.backendSpaceHandle().value(), - toRuntimeJointTypeCode(state.getType()), - bodyA.backendBodyHandle().value(), - bodyB.backendBodyHandle().value(), - anchorA.x, - anchorA.y, - anchorA.z, - anchorB.x, - anchorB.y, - anchorB.z, - axis.x, - axis.y, - axis.z, - state.getSpringRestLength(), - state.getSpringStiffness(), - state.getSpringDamping(), - state.getLowerLimit(), - state.getUpperLimit(), - state.isMotorEnabled(), - state.getMotorTargetVelocity(), - state.getMotorMaxForce())); - JointKey jointKey = JointKey.random(); - try { - runtime.addJointOnOwner(jointKey, - space.spaceId(), - backendJointHandle, - bodyAKey, - bodyBKey, - type, - anchorA.x, - anchorA.y, - anchorA.z, - anchorB.x, - anchorB.y, - anchorB.z, - axis.x, - axis.y, - axis.z, - state.getSpringRestLength(), - state.getSpringStiffness(), - state.getSpringDamping(), - state.getLowerLimit(), - state.getUpperLimit(), - state.isMotorEnabled(), - state.getMotorTargetVelocity(), - state.getMotorMaxForce()); - } catch (RuntimeException exception) { - removeBackendJoint(space, backendJointHandle, exception); - throw exception; - } - return jointKey; - } - - private static void removeBackendJoint(@Nonnull PhysicsSpaceBinding space, - @Nonnull BackendJointHandle backendJointHandle, - @Nonnull RuntimeException restoreFailure) { - try { - space.runtime().removeJoint(space.backendSpaceHandle().value(), backendJointHandle.value()); - } catch (RuntimeException cleanupFailure) { - restoreFailure.addSuppressed(cleanupFailure); - } - } - - @Nonnull - private static Vector3f requireAxis(@Nonnull PersistentPhysicsJointState state) { - Vector3f axis = state.getAxis(); - if (axis == null) { - throw new IllegalStateException("Persisted " + state.getType() + " joint requires an axis"); - } - return new Vector3f(axis); - } - - private static int toRuntimeJointTypeCode(@Nonnull PhysicsJointType type) { - return switch (type) { - case FIXED -> BackendRuntimeCodes.JOINT_FIXED; - case POINT -> BackendRuntimeCodes.JOINT_POINT; - case HINGE -> BackendRuntimeCodes.JOINT_HINGE; - case SLIDER -> BackendRuntimeCodes.JOINT_SLIDER; - case SPRING -> BackendRuntimeCodes.JOINT_SPRING; - }; - } - - @Nonnull - private static JointType toRuntimeJointType(@Nonnull PhysicsJointType type) { - return switch (type) { - case FIXED -> JointType.FIXED; - case POINT -> JointType.POINT; - case HINGE -> JointType.HINGE; - case SLIDER -> JointType.SLIDER; - case SPRING -> JointType.SPRING; - }; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsSpaceState.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsSpaceState.java deleted file mode 100644 index 55e13878..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsSpaceState.java +++ /dev/null @@ -1,640 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.persistence; - -import com.hypixel.hytale.codec.Codec; -import com.hypixel.hytale.codec.ExtraInfo; -import com.hypixel.hytale.codec.KeyedCodec; -import com.hypixel.hytale.codec.builder.BuilderCodec; -import com.hypixel.hytale.codec.codecs.EnumCodec; -import com.hypixel.hytale.codec.codecs.array.ArrayCodec; -import com.hypixel.hytale.codec.validation.ValidationResults; -import com.hypixel.hytale.codec.validation.Validators; -import com.hypixel.hytale.math.vector.Vector3fUtil; -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.systems.persistence.PersistentPhysicsSpaceBootstrapSystem; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionMode; -import dev.hytalemodding.impulse.core.plugin.settings.EntityChunkBoundaryMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsCollisionLodSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSolverSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsVisualMaterializationSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsVisualSyncSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldCollisionSettings; -import dev.hytalemodding.impulse.core.plugin.settings.VisualOcclusionMode; -import javax.annotation.Nonnull; -import lombok.Getter; -import lombok.Setter; -import org.joml.Vector3f; - -/** - * Codec-backed definition of one physics space for the persistence layer. - * - *

Captures the space identity, backend choice, gravity, and world-collision - * settings so that {@link PersistentPhysicsSpaceBootstrapSystem} can recreate - * the runtime physics space after a - * world load or manual snapshot restore.

- */ -@Getter -public class PersistentPhysicsSpaceState { - - private static final PersistentPhysicsExtensionSettingState[] EMPTY_EXTENSION_SETTINGS = - new PersistentPhysicsExtensionSettingState[0]; - - @Nonnull - public static final BuilderCodec CODEC = BuilderCodec.builder( - PersistentPhysicsSpaceState.class, - PersistentPhysicsSpaceState::new) - .append(new KeyedCodec<>("SpaceId", Codec.INTEGER), - (state, value) -> state.spaceId = value, - PersistentPhysicsSpaceState::getSpaceId) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("BackendId", Codec.STRING), - (state, value) -> state.backendId = value, - PersistentPhysicsSpaceState::getBackendId) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("Gravity", Vector3fUtil.CODEC), - (state, value) -> state.gravity.set(value), - PersistentPhysicsSpaceState::getGravity) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.finiteVector( - "Persisted space gravity must be finite")) - .add() - .append(new KeyedCodec<>("WorldCollisionMode", new EnumCodec<>(WorldCollisionMode.class), false), - (state, value) -> state.worldCollisionMode = value, - PersistentPhysicsSpaceState::getWorldCollisionMode) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("EntityChunkBoundaryMode", - new EnumCodec<>(EntityChunkBoundaryMode.class), false), - (state, value) -> state.entityChunkBoundaryMode = value, - PersistentPhysicsSpaceState::getEntityChunkBoundaryMode) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("WorldCollisionRadius", Codec.INTEGER, false), - (state, value) -> state.worldCollisionRadius = value, - PersistentPhysicsSpaceState::getWorldCollisionRadius) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsWorldCollisionSettings.MAX_WORLD_COLLISION_RADIUS)) - .add() - .append(new KeyedCodec<>("WorldCollisionBodyRadius", Codec.INTEGER, false), - (state, value) -> state.worldCollisionBodyRadius = value, - PersistentPhysicsSpaceState::getWorldCollisionBodyRadius) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsWorldCollisionSettings.MAX_WORLD_COLLISION_BODY_RADIUS)) - .add() - .append(new KeyedCodec<>("WorldCollisionTtlTicks", Codec.INTEGER, false), - (state, value) -> state.worldCollisionTtlTicks = value, - PersistentPhysicsSpaceState::getWorldCollisionTtlTicks) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsWorldCollisionSettings.MAX_WORLD_COLLISION_TTL_TICKS)) - .add() - .append(new KeyedCodec<>("NativeVoxelTerrainEnabled", Codec.BOOLEAN, false), - (state, value) -> state.nativeVoxelTerrainEnabled = value, - PersistentPhysicsSpaceState::isNativeVoxelTerrainEnabled) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("TerrainFriction", Codec.FLOAT, false), - (state, value) -> state.terrainFriction = value, - PersistentPhysicsSpaceState::getTerrainFriction) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(0.0f, Float.MAX_VALUE)) - .add() - .append(new KeyedCodec<>("TerrainRestitution", Codec.FLOAT, false), - (state, value) -> state.terrainRestitution = value, - PersistentPhysicsSpaceState::getTerrainRestitution) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(0.0f, Float.MAX_VALUE)) - .add() - .append(new KeyedCodec<>("VisualFullSyncRadius", Codec.INTEGER, false), - (state, value) -> state.visualFullSyncRadius = value, - PersistentPhysicsSpaceState::getVisualFullSyncRadius) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsVisualSyncSettings.MAX_VISUAL_FULL_SYNC_RADIUS)) - .add() - .append(new KeyedCodec<>("VisualMaxSyncRadius", Codec.INTEGER, false), - (state, value) -> state.visualMaxSyncRadius = value, - PersistentPhysicsSpaceState::getVisualMaxSyncRadius) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsVisualSyncSettings.MAX_VISUAL_MAX_SYNC_RADIUS)) - .add() - .append(new KeyedCodec<>("VisualFarSyncCutoffEnabled", Codec.BOOLEAN, false), - (state, value) -> state.visualFarSyncCutoffEnabled = value, - PersistentPhysicsSpaceState::isVisualFarSyncCutoffEnabled) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("VisualMidSyncIntervalTicks", Codec.INTEGER, false), - (state, value) -> state.visualMidSyncIntervalTicks = value, - PersistentPhysicsSpaceState::getVisualMidSyncIntervalTicks) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsVisualSyncSettings.MAX_VISUAL_MID_SYNC_INTERVAL_TICKS)) - .add() - .append(new KeyedCodec<>("VisualFarSyncIntervalTicks", Codec.INTEGER, false), - (state, value) -> state.visualFarSyncIntervalTicks = value, - PersistentPhysicsSpaceState::getVisualFarSyncIntervalTicks) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsVisualSyncSettings.MAX_VISUAL_FAR_SYNC_INTERVAL_TICKS)) - .add() - .append(new KeyedCodec<>("VisualOcclusionMode", new EnumCodec<>(VisualOcclusionMode.class), false), - (state, value) -> state.visualOcclusionMode = value, - PersistentPhysicsSpaceState::getVisualOcclusionMode) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("VisualOcclusionRaycastsPerTick", Codec.INTEGER, false), - (state, value) -> state.visualOcclusionRaycastsPerTick = value, - PersistentPhysicsSpaceState::getVisualOcclusionRaycastsPerTick) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsVisualSyncSettings.MAX_VISUAL_OCCLUSION_RAYCASTS_PER_TICK)) - .add() - .append(new KeyedCodec<>("VisualOcclusionCacheTicks", Codec.INTEGER, false), - (state, value) -> state.visualOcclusionCacheTicks = value, - PersistentPhysicsSpaceState::getVisualOcclusionCacheTicks) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsVisualSyncSettings.MAX_VISUAL_OCCLUSION_CACHE_TICKS)) - .add() - .append(new KeyedCodec<>("VisualSnapshotPredictionEnabled", Codec.BOOLEAN, false), - (state, value) -> state.visualSnapshotPredictionEnabled = value, - PersistentPhysicsSpaceState::isVisualSnapshotPredictionEnabled) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("VisualSnapshotPredictionMaxSeconds", Codec.FLOAT, false), - (state, value) -> state.visualSnapshotPredictionMaxSeconds = value, - PersistentPhysicsSpaceState::getVisualSnapshotPredictionMaxSeconds) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(0.0f, PhysicsVisualSyncSettings.MAX_VISUAL_SNAPSHOT_PREDICTION_MAX_SECONDS)) - .add() - .append(new KeyedCodec<>("VisualSnapshotSmoothingEnabled", Codec.BOOLEAN, false), - (state, value) -> state.visualSnapshotSmoothingEnabled = value, - PersistentPhysicsSpaceState::isVisualSnapshotSmoothingEnabled) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("VisualSnapshotSmoothingRate", Codec.FLOAT, false), - (state, value) -> state.visualSnapshotSmoothingRate = value, - PersistentPhysicsSpaceState::getVisualSnapshotSmoothingRate) - .addValidator(Validators.nonNull()) - .addValidator(Validators.greaterThan(0.0f)) - .addValidator(Validators.max(PhysicsVisualSyncSettings.MAX_VISUAL_SNAPSHOT_SMOOTHING_RATE)) - .add() - .append(new KeyedCodec<>("SolverIterations", Codec.INTEGER, false), - (state, value) -> state.solverIterations = value, - PersistentPhysicsSpaceState::getSolverIterations) - .addValidator(Validators.nonNull()) - .addValidator(Validators.min(1)) - .add() - .append(new KeyedCodec<>("StabilizationIterations", Codec.INTEGER, false), - (state, value) -> state.stabilizationIterations = value, - PersistentPhysicsSpaceState::getStabilizationIterations) - .addValidator(Validators.nonNull()) - .addValidator(Validators.min(0)) - .add() - .append(new KeyedCodec<>("DynamicSleepLinearThreshold", Codec.FLOAT, false), - (state, value) -> state.dynamicSleepLinearThreshold = value, - PersistentPhysicsSpaceState::getDynamicSleepLinearThreshold) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(0.0f, Float.MAX_VALUE)) - .add() - .append(new KeyedCodec<>("DynamicSleepAngularThreshold", Codec.FLOAT, false), - (state, value) -> state.dynamicSleepAngularThreshold = value, - PersistentPhysicsSpaceState::getDynamicSleepAngularThreshold) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(0.0f, Float.MAX_VALUE)) - .add() - .append(new KeyedCodec<>("DynamicSleepTimeUntilSleep", Codec.FLOAT, false), - (state, value) -> state.dynamicSleepTimeUntilSleep = value, - PersistentPhysicsSpaceState::getDynamicSleepTimeUntilSleep) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(0.0f, Float.MAX_VALUE)) - .add() - .append(new KeyedCodec<>("ExtensionSettings", - new ArrayCodec<>(PersistentPhysicsExtensionSettingState.CODEC, - PersistentPhysicsExtensionSettingState[]::new), - false), - (state, value) -> state.extensionSettings = copyExtensionSettings(value), - PersistentPhysicsSpaceState::getExtensionSettings) - .addValidator(Validators.nonNull()) - .addValidator(Validators.nonNullArrayElements()) - .add() - .append(new KeyedCodec<>("EntityVisualSyncCullingEnabled", Codec.BOOLEAN, false), - (state, value) -> state.entityVisualSyncCullingEnabled = value, - PersistentPhysicsSpaceState::isEntityVisualSyncCullingEnabled) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("VisualVisibilityCullingEnabled", Codec.BOOLEAN, false), - (state, value) -> state.visualVisibilityCullingEnabled = value, - PersistentPhysicsSpaceState::isVisualVisibilityCullingEnabled) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("DetachedVisualMaterializationEnabled", Codec.BOOLEAN, false), - (state, value) -> state.detachedVisualMaterializationEnabled = value, - PersistentPhysicsSpaceState::isDetachedVisualMaterializationEnabled) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("DetachedVisualMaterializationRadius", Codec.INTEGER, false), - (state, value) -> state.detachedVisualMaterializationRadius = value, - PersistentPhysicsSpaceState::getDetachedVisualMaterializationRadius) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsVisualMaterializationSettings.MAX_DETACHED_VISUAL_MATERIALIZATION_RADIUS)) - .add() - .append(new KeyedCodec<>("DetachedVisualDematerializationRadius", Codec.INTEGER, false), - (state, value) -> state.detachedVisualDematerializationRadius = value, - PersistentPhysicsSpaceState::getDetachedVisualDematerializationRadius) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsVisualMaterializationSettings.MAX_DETACHED_VISUAL_DEMATERIALIZATION_RADIUS)) - .add() - .append(new KeyedCodec<>("DetachedVisualMaxSpawnsPerTick", Codec.INTEGER, false), - (state, value) -> state.detachedVisualMaxSpawnsPerTick = value, - PersistentPhysicsSpaceState::getDetachedVisualMaxSpawnsPerTick) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsVisualMaterializationSettings.MAX_DETACHED_VISUAL_MAX_SPAWNS_PER_TICK)) - .add() - .append(new KeyedCodec<>("DetachedVisualMaxMaterialized", Codec.INTEGER, false), - (state, value) -> state.detachedVisualMaxMaterialized = value, - PersistentPhysicsSpaceState::getDetachedVisualMaxMaterialized) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsVisualMaterializationSettings.MAX_DETACHED_VISUAL_MAX_MATERIALIZED)) - .add() - .append(new KeyedCodec<>("DetachedVisualInterestRefreshIntervalTicks", Codec.INTEGER, false), - (state, value) -> state.detachedVisualInterestRefreshIntervalTicks = value, - PersistentPhysicsSpaceState::getDetachedVisualInterestRefreshIntervalTicks) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsVisualMaterializationSettings.MAX_DETACHED_VISUAL_CACHE_INTERVAL_TICKS)) - .add() - .append(new KeyedCodec<>("DetachedVisualCandidateRefreshIntervalTicks", Codec.INTEGER, false), - (state, value) -> state.detachedVisualCandidateRefreshIntervalTicks = value, - PersistentPhysicsSpaceState::getDetachedVisualCandidateRefreshIntervalTicks) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsVisualMaterializationSettings.MAX_DETACHED_VISUAL_CACHE_INTERVAL_TICKS)) - .add() - .append(new KeyedCodec<>("DetachedVisualVisibilityCheckIntervalTicks", Codec.INTEGER, false), - (state, value) -> state.detachedVisualVisibilityCheckIntervalTicks = value, - PersistentPhysicsSpaceState::getDetachedVisualVisibilityCheckIntervalTicks) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsVisualMaterializationSettings.MAX_DETACHED_VISUAL_CACHE_INTERVAL_TICKS)) - .add() - .append(new KeyedCodec<>("CollisionLodEnabled", Codec.BOOLEAN, false), - (state, value) -> state.collisionLodEnabled = value, - PersistentPhysicsSpaceState::isCollisionLodEnabled) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("CollisionLodNearRadius", Codec.INTEGER, false), - (state, value) -> state.collisionLodNearRadius = value, - PersistentPhysicsSpaceState::getCollisionLodNearRadius) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsCollisionLodSettings.MAX_COLLISION_LOD_RADIUS)) - .add() - .append(new KeyedCodec<>("CollisionLodMidRadius", Codec.INTEGER, false), - (state, value) -> state.collisionLodMidRadius = value, - PersistentPhysicsSpaceState::getCollisionLodMidRadius) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsCollisionLodSettings.MAX_COLLISION_LOD_RADIUS)) - .add() - .append(new KeyedCodec<>("CollisionLodHysteresis", Codec.INTEGER, false), - (state, value) -> state.collisionLodHysteresis = value, - PersistentPhysicsSpaceState::getCollisionLodHysteresis) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(0, PhysicsCollisionLodSettings.MAX_COLLISION_LOD_HYSTERESIS)) - .add() - .append(new KeyedCodec<>("CollisionLodRefreshIntervalTicks", Codec.INTEGER, false), - (state, value) -> state.collisionLodRefreshIntervalTicks = value, - PersistentPhysicsSpaceState::getCollisionLodRefreshIntervalTicks) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, PhysicsCollisionLodSettings.MAX_COLLISION_LOD_REFRESH_INTERVAL_TICKS)) - .add() - .append(new KeyedCodec<>("CollisionLodFarSleepEnabled", Codec.BOOLEAN, false), - (state, value) -> state.collisionLodFarSleepEnabled = value, - PersistentPhysicsSpaceState::isCollisionLodFarSleepEnabled) - .addValidator(Validators.nonNull()) - .add() - .append(new KeyedCodec<>("DetachedVisualBlockType", Codec.STRING, false), - (state, value) -> state.detachedVisualBlockType = value, - PersistentPhysicsSpaceState::getDetachedVisualBlockType) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.nonBlankString( - "Persisted detached visual block type cannot be blank")) - .add() - .afterDecode(PersistentPhysicsSpaceState::validateAfterDecode) - .build(); - - @Setter - private int spaceId; - @Nonnull - @Setter - private String backendId = ""; - @Nonnull - private final Vector3f gravity = new Vector3f(0.0f, -32f, 0.0f);// new Vector3f(0.0f, -9.81f, 0.0f); - @Nonnull - @Setter - private WorldCollisionMode worldCollisionMode = WorldCollisionMode.NONE; - @Nonnull - @Setter - private EntityChunkBoundaryMode entityChunkBoundaryMode = - PhysicsWorldCollisionSettings.DEFAULT_ENTITY_CHUNK_BOUNDARY_MODE; - private int worldCollisionRadius = PhysicsWorldCollisionSettings.DEFAULT_WORLD_COLLISION_RADIUS; - private int worldCollisionBodyRadius = PhysicsWorldCollisionSettings.DEFAULT_WORLD_COLLISION_BODY_RADIUS; - private int worldCollisionTtlTicks = PhysicsWorldCollisionSettings.DEFAULT_WORLD_COLLISION_TTL_TICKS; - @Setter - private boolean nativeVoxelTerrainEnabled = - PhysicsWorldCollisionSettings.DEFAULT_NATIVE_VOXEL_TERRAIN_ENABLED; - private float terrainFriction = PhysicsWorldCollisionSettings.DEFAULT_TERRAIN_FRICTION; - private float terrainRestitution = PhysicsWorldCollisionSettings.DEFAULT_TERRAIN_RESTITUTION; - private int visualFullSyncRadius = PhysicsVisualSyncSettings.DEFAULT_VISUAL_FULL_SYNC_RADIUS; - private int visualMaxSyncRadius = PhysicsVisualSyncSettings.DEFAULT_VISUAL_MAX_SYNC_RADIUS; - @Setter - private boolean visualFarSyncCutoffEnabled = - PhysicsVisualSyncSettings.DEFAULT_VISUAL_FAR_SYNC_CUTOFF_ENABLED; - private int visualMidSyncIntervalTicks = - PhysicsVisualSyncSettings.DEFAULT_VISUAL_MID_SYNC_INTERVAL_TICKS; - private int visualFarSyncIntervalTicks = - PhysicsVisualSyncSettings.DEFAULT_VISUAL_FAR_SYNC_INTERVAL_TICKS; - @Nonnull - @Setter - private VisualOcclusionMode visualOcclusionMode = - PhysicsVisualSyncSettings.DEFAULT_VISUAL_OCCLUSION_MODE; - private int visualOcclusionRaycastsPerTick = - PhysicsVisualSyncSettings.DEFAULT_VISUAL_OCCLUSION_RAYCASTS_PER_TICK; - private int visualOcclusionCacheTicks = - PhysicsVisualSyncSettings.DEFAULT_VISUAL_OCCLUSION_CACHE_TICKS; - @Setter - private boolean visualSnapshotPredictionEnabled = - PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_PREDICTION_ENABLED; - private float visualSnapshotPredictionMaxSeconds = - PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_PREDICTION_MAX_SECONDS; - @Setter - private boolean visualSnapshotSmoothingEnabled = - PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_SMOOTHING_ENABLED; - private float visualSnapshotSmoothingRate = - PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_SMOOTHING_RATE; - @Setter - private int solverIterations = PhysicsSolverSettings.DEFAULT_SOLVER_ITERATIONS; - @Setter - private int stabilizationIterations = PhysicsSolverSettings.DEFAULT_STABILIZATION_ITERATIONS; - @Setter - private float dynamicSleepLinearThreshold = - PhysicsSolverSettings.DEFAULT_DYNAMIC_SLEEP_LINEAR_THRESHOLD; - @Setter - private float dynamicSleepAngularThreshold = - PhysicsSolverSettings.DEFAULT_DYNAMIC_SLEEP_ANGULAR_THRESHOLD; - @Setter - private float dynamicSleepTimeUntilSleep = - PhysicsSolverSettings.DEFAULT_DYNAMIC_SLEEP_TIME_UNTIL_SLEEP; - @Nonnull - private PersistentPhysicsExtensionSettingState[] extensionSettings = EMPTY_EXTENSION_SETTINGS; - @Setter - private boolean entityVisualSyncCullingEnabled = - PhysicsVisualSyncSettings.DEFAULT_ENTITY_VISUAL_SYNC_CULLING_ENABLED; - @Setter - private boolean visualVisibilityCullingEnabled = - PhysicsVisualSyncSettings.DEFAULT_VISUAL_VISIBILITY_CULLING_ENABLED; - @Setter - private boolean detachedVisualMaterializationEnabled = - PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_MATERIALIZATION_ENABLED; - private int detachedVisualMaterializationRadius = - PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_MATERIALIZATION_RADIUS; - private int detachedVisualDematerializationRadius = - PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_DEMATERIALIZATION_RADIUS; - private int detachedVisualMaxSpawnsPerTick = - PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_MAX_SPAWNS_PER_TICK; - private int detachedVisualMaxMaterialized = - PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_MAX_MATERIALIZED; - private int detachedVisualInterestRefreshIntervalTicks = - PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_INTEREST_REFRESH_INTERVAL_TICKS; - private int detachedVisualCandidateRefreshIntervalTicks = - PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_CANDIDATE_REFRESH_INTERVAL_TICKS; - private int detachedVisualVisibilityCheckIntervalTicks = - PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_VISIBILITY_CHECK_INTERVAL_TICKS; - @Setter - private boolean collisionLodEnabled = - PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_ENABLED; - private int collisionLodNearRadius = - PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_NEAR_RADIUS; - private int collisionLodMidRadius = - PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_MID_RADIUS; - private int collisionLodHysteresis = - PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_HYSTERESIS; - private int collisionLodRefreshIntervalTicks = - PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_REFRESH_INTERVAL_TICKS; - @Setter - private boolean collisionLodFarSleepEnabled = - PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_FAR_SLEEP_ENABLED; - @Nonnull - @Setter - private String detachedVisualBlockType = - PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_BLOCK_TYPE; - - public PersistentPhysicsSpaceState() { - } - - @Nonnull - public static PersistentPhysicsSpaceState from(@Nonnull PhysicsSpaceBinding space, - @Nonnull PhysicsSpaceSettings settings) { - PersistentPhysicsSpaceState state = new PersistentPhysicsSpaceState(); - state.spaceId = space.spaceId().value(); - state.backendId = space.backendId().value(); - space.runtime().getGravity(space.backendSpaceHandle().value(), state.gravity::set); - state.worldCollisionMode = settings.getWorldCollisionSettings().getWorldCollisionMode(); - state.entityChunkBoundaryMode = settings.getWorldCollisionSettings().getEntityChunkBoundaryMode(); - state.worldCollisionRadius = settings.getWorldCollisionSettings().getWorldCollisionRadius(); - state.worldCollisionBodyRadius = settings.getWorldCollisionSettings().getWorldCollisionBodyRadius(); - state.worldCollisionTtlTicks = settings.getWorldCollisionSettings().getWorldCollisionTtlTicks(); - state.nativeVoxelTerrainEnabled = settings.getWorldCollisionSettings().isNativeVoxelTerrainEnabled(); - state.terrainFriction = settings.getWorldCollisionSettings().getTerrainFriction(); - state.terrainRestitution = settings.getWorldCollisionSettings().getTerrainRestitution(); - state.visualFullSyncRadius = settings.getVisualSyncSettings().getVisualFullSyncRadius(); - state.visualMaxSyncRadius = settings.getVisualSyncSettings().getVisualMaxSyncRadius(); - state.visualFarSyncCutoffEnabled = settings.getVisualSyncSettings().isVisualFarSyncCutoffEnabled(); - state.visualMidSyncIntervalTicks = settings.getVisualSyncSettings().getVisualMidSyncIntervalTicks(); - state.visualFarSyncIntervalTicks = settings.getVisualSyncSettings().getVisualFarSyncIntervalTicks(); - state.visualOcclusionMode = settings.getVisualSyncSettings().getVisualOcclusionMode(); - state.visualOcclusionRaycastsPerTick = settings.getVisualSyncSettings().getVisualOcclusionRaycastsPerTick(); - state.visualOcclusionCacheTicks = settings.getVisualSyncSettings().getVisualOcclusionCacheTicks(); - state.visualSnapshotPredictionEnabled = settings.getVisualSyncSettings().isVisualSnapshotPredictionEnabled(); - state.visualSnapshotPredictionMaxSeconds = settings.getVisualSyncSettings().getVisualSnapshotPredictionMaxSeconds(); - state.visualSnapshotSmoothingEnabled = settings.getVisualSyncSettings().isVisualSnapshotSmoothingEnabled(); - state.visualSnapshotSmoothingRate = settings.getVisualSyncSettings().getVisualSnapshotSmoothingRate(); - state.solverIterations = settings.getSolverSettings().getSolverIterations(); - state.stabilizationIterations = settings.getSolverSettings().getStabilizationIterations(); - state.dynamicSleepLinearThreshold = settings.getSolverSettings().getDynamicSleepLinearThreshold(); - state.dynamicSleepAngularThreshold = settings.getSolverSettings().getDynamicSleepAngularThreshold(); - state.dynamicSleepTimeUntilSleep = settings.getSolverSettings().getDynamicSleepTimeUntilSleep(); - state.extensionSettings = extensionSettingsFrom(settings); - state.entityVisualSyncCullingEnabled = settings.getVisualSyncSettings().isEntityVisualSyncCullingEnabled(); - state.visualVisibilityCullingEnabled = settings.getVisualSyncSettings().isVisualVisibilityCullingEnabled(); - state.detachedVisualMaterializationEnabled = settings.getVisualMaterializationSettings().isDetachedVisualMaterializationEnabled(); - state.detachedVisualMaterializationRadius = settings.getVisualMaterializationSettings().getDetachedVisualMaterializationRadius(); - state.detachedVisualDematerializationRadius = settings.getVisualMaterializationSettings().getDetachedVisualDematerializationRadius(); - state.detachedVisualMaxSpawnsPerTick = settings.getVisualMaterializationSettings().getDetachedVisualMaxSpawnsPerTick(); - state.detachedVisualMaxMaterialized = settings.getVisualMaterializationSettings().getDetachedVisualMaxMaterialized(); - state.detachedVisualInterestRefreshIntervalTicks = - settings.getVisualMaterializationSettings().getDetachedVisualInterestRefreshIntervalTicks(); - state.detachedVisualCandidateRefreshIntervalTicks = - settings.getVisualMaterializationSettings().getDetachedVisualCandidateRefreshIntervalTicks(); - state.detachedVisualVisibilityCheckIntervalTicks = - settings.getVisualMaterializationSettings().getDetachedVisualVisibilityCheckIntervalTicks(); - state.collisionLodEnabled = settings.getCollisionLodSettings().isCollisionLodEnabled(); - state.collisionLodNearRadius = settings.getCollisionLodSettings().getCollisionLodNearRadius(); - state.collisionLodMidRadius = settings.getCollisionLodSettings().getCollisionLodMidRadius(); - state.collisionLodHysteresis = settings.getCollisionLodSettings().getCollisionLodHysteresis(); - state.collisionLodRefreshIntervalTicks = settings.getCollisionLodSettings().getCollisionLodRefreshIntervalTicks(); - state.collisionLodFarSleepEnabled = settings.getCollisionLodSettings().isCollisionLodFarSleepEnabled(); - state.detachedVisualBlockType = settings.getVisualMaterializationSettings().getDetachedVisualBlockType(); - return state; - } - - @Nonnull - public SpaceId toSpaceId() { - return new SpaceId(spaceId); - } - - @Nonnull - public BackendId toBackendId() { - return new BackendId(backendId); - } - - @Nonnull - public PhysicsSpaceSettings toSettings() { - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - settings.getWorldCollisionSettings().setWorldCollisionMode(worldCollisionMode); - settings.getWorldCollisionSettings().setEntityChunkBoundaryMode(entityChunkBoundaryMode); - settings.getWorldCollisionSettings().setWorldCollisionRadius(worldCollisionRadius); - settings.getWorldCollisionSettings().setWorldCollisionBodyRadius(worldCollisionBodyRadius); - settings.getWorldCollisionSettings().setWorldCollisionTtlTicks(worldCollisionTtlTicks); - settings.getWorldCollisionSettings().setNativeVoxelTerrainEnabled(nativeVoxelTerrainEnabled); - settings.getWorldCollisionSettings().setTerrainMaterial(terrainFriction, terrainRestitution); - settings.getVisualSyncSettings().setVisualSyncRadii(visualFullSyncRadius, visualMaxSyncRadius); - settings.getVisualSyncSettings().setVisualFarSyncCutoffEnabled(visualFarSyncCutoffEnabled); - settings.getVisualSyncSettings().setVisualMidSyncIntervalTicks(visualMidSyncIntervalTicks); - settings.getVisualSyncSettings().setVisualFarSyncIntervalTicks(visualFarSyncIntervalTicks); - settings.getVisualSyncSettings().setVisualOcclusionMode(visualOcclusionMode); - settings.getVisualSyncSettings().setVisualOcclusionRaycastsPerTick(visualOcclusionRaycastsPerTick); - settings.getVisualSyncSettings().setVisualOcclusionCacheTicks(visualOcclusionCacheTicks); - settings.getVisualSyncSettings().setVisualSnapshotPredictionEnabled(visualSnapshotPredictionEnabled); - settings.getVisualSyncSettings().setVisualSnapshotPredictionMaxSeconds(visualSnapshotPredictionMaxSeconds); - settings.getVisualSyncSettings().setVisualSnapshotSmoothingEnabled(visualSnapshotSmoothingEnabled); - settings.getVisualSyncSettings().setVisualSnapshotSmoothingRate(visualSnapshotSmoothingRate); - settings.getSolverSettings().setSolverIterations(solverIterations); - settings.getSolverSettings().setStabilizationIterations(stabilizationIterations); - settings.getSolverSettings().setDynamicSleepTuning(dynamicSleepLinearThreshold, - dynamicSleepAngularThreshold, - dynamicSleepTimeUntilSleep); - for (PersistentPhysicsExtensionSettingState extensionSetting : extensionSettings) { - extensionSetting.applyTo(settings.getExtensionSettings()); - } - settings.getVisualSyncSettings().setEntityVisualSyncCullingEnabled(entityVisualSyncCullingEnabled); - settings.getVisualSyncSettings().setVisualVisibilityCullingEnabled(visualVisibilityCullingEnabled); - settings.getVisualMaterializationSettings().setDetachedVisualMaterializationEnabled(detachedVisualMaterializationEnabled); - settings.getVisualMaterializationSettings().setDetachedVisualRadii( - detachedVisualMaterializationRadius, - detachedVisualDematerializationRadius); - settings.getVisualMaterializationSettings().setDetachedVisualMaxSpawnsPerTick(detachedVisualMaxSpawnsPerTick); - settings.getVisualMaterializationSettings().setDetachedVisualMaxMaterialized(detachedVisualMaxMaterialized); - settings.getVisualMaterializationSettings().setDetachedVisualInterestRefreshIntervalTicks( - detachedVisualInterestRefreshIntervalTicks); - settings.getVisualMaterializationSettings().setDetachedVisualCandidateRefreshIntervalTicks( - detachedVisualCandidateRefreshIntervalTicks); - settings.getVisualMaterializationSettings().setDetachedVisualVisibilityCheckIntervalTicks( - detachedVisualVisibilityCheckIntervalTicks); - settings.getCollisionLodSettings().setCollisionLodEnabled(collisionLodEnabled); - settings.getCollisionLodSettings().setCollisionLodRadii(collisionLodNearRadius, collisionLodMidRadius); - settings.getCollisionLodSettings().setCollisionLodHysteresis(collisionLodHysteresis); - settings.getCollisionLodSettings().setCollisionLodRefreshIntervalTicks(collisionLodRefreshIntervalTicks); - settings.getCollisionLodSettings().setCollisionLodFarSleepEnabled(collisionLodFarSleepEnabled); - settings.getVisualMaterializationSettings().setDetachedVisualBlockType(detachedVisualBlockType); - return settings; - } - - private static void validate(@Nonnull PersistentPhysicsSpaceState state, - @Nonnull ValidationResults results) { - try { - state.toSettings(); - } catch (RuntimeException exception) { - results.fail("Invalid persisted space settings: " + exception.getMessage()); - } - } - - private static void validateAfterDecode(@Nonnull PersistentPhysicsSpaceState state, - @Nonnull ExtraInfo extraInfo) { - validate(state, extraInfo.getValidationResults()); - } - - @Nonnull - public PersistentPhysicsSpaceState copy() { - PersistentPhysicsSpaceState copy = new PersistentPhysicsSpaceState(); - copy.spaceId = spaceId; - copy.backendId = backendId; - copy.gravity.set(gravity); - copy.worldCollisionMode = worldCollisionMode; - copy.entityChunkBoundaryMode = entityChunkBoundaryMode; - copy.worldCollisionRadius = worldCollisionRadius; - copy.worldCollisionBodyRadius = worldCollisionBodyRadius; - copy.worldCollisionTtlTicks = worldCollisionTtlTicks; - copy.nativeVoxelTerrainEnabled = nativeVoxelTerrainEnabled; - copy.terrainFriction = terrainFriction; - copy.terrainRestitution = terrainRestitution; - copy.visualFullSyncRadius = visualFullSyncRadius; - copy.visualMaxSyncRadius = visualMaxSyncRadius; - copy.visualFarSyncCutoffEnabled = visualFarSyncCutoffEnabled; - copy.visualMidSyncIntervalTicks = visualMidSyncIntervalTicks; - copy.visualFarSyncIntervalTicks = visualFarSyncIntervalTicks; - copy.visualOcclusionMode = visualOcclusionMode; - copy.visualOcclusionRaycastsPerTick = visualOcclusionRaycastsPerTick; - copy.visualOcclusionCacheTicks = visualOcclusionCacheTicks; - copy.visualSnapshotPredictionEnabled = visualSnapshotPredictionEnabled; - copy.visualSnapshotPredictionMaxSeconds = visualSnapshotPredictionMaxSeconds; - copy.visualSnapshotSmoothingEnabled = visualSnapshotSmoothingEnabled; - copy.visualSnapshotSmoothingRate = visualSnapshotSmoothingRate; - copy.solverIterations = solverIterations; - copy.stabilizationIterations = stabilizationIterations; - copy.dynamicSleepLinearThreshold = dynamicSleepLinearThreshold; - copy.dynamicSleepAngularThreshold = dynamicSleepAngularThreshold; - copy.dynamicSleepTimeUntilSleep = dynamicSleepTimeUntilSleep; - copy.extensionSettings = copyExtensionSettings(extensionSettings); - copy.entityVisualSyncCullingEnabled = entityVisualSyncCullingEnabled; - copy.visualVisibilityCullingEnabled = visualVisibilityCullingEnabled; - copy.detachedVisualMaterializationEnabled = detachedVisualMaterializationEnabled; - copy.detachedVisualMaterializationRadius = detachedVisualMaterializationRadius; - copy.detachedVisualDematerializationRadius = detachedVisualDematerializationRadius; - copy.detachedVisualMaxSpawnsPerTick = detachedVisualMaxSpawnsPerTick; - copy.detachedVisualMaxMaterialized = detachedVisualMaxMaterialized; - copy.detachedVisualInterestRefreshIntervalTicks = - detachedVisualInterestRefreshIntervalTicks; - copy.detachedVisualCandidateRefreshIntervalTicks = - detachedVisualCandidateRefreshIntervalTicks; - copy.detachedVisualVisibilityCheckIntervalTicks = - detachedVisualVisibilityCheckIntervalTicks; - copy.collisionLodEnabled = collisionLodEnabled; - copy.collisionLodNearRadius = collisionLodNearRadius; - copy.collisionLodMidRadius = collisionLodMidRadius; - copy.collisionLodHysteresis = collisionLodHysteresis; - copy.collisionLodRefreshIntervalTicks = collisionLodRefreshIntervalTicks; - copy.collisionLodFarSleepEnabled = collisionLodFarSleepEnabled; - copy.detachedVisualBlockType = detachedVisualBlockType; - return copy; - } - - @Nonnull - private static PersistentPhysicsExtensionSettingState[] extensionSettingsFrom( - @Nonnull PhysicsSpaceSettings settings) { - return settings.getExtensionSettings().asMap().entrySet().stream() - .flatMap(entry -> entry.getValue().entrySet().stream() - .map(setting -> PersistentPhysicsExtensionSettingState.from(entry.getKey(), - setting.getKey(), - setting.getValue()))) - .toArray(PersistentPhysicsExtensionSettingState[]::new); - } - - @Nonnull - private static PersistentPhysicsExtensionSettingState[] copyExtensionSettings( - @Nonnull PersistentPhysicsExtensionSettingState[] settings) { - if (settings.length == 0) { - return EMPTY_EXTENSION_SETTINGS; - } - PersistentPhysicsExtensionSettingState[] copy = - new PersistentPhysicsExtensionSettingState[settings.length]; - for (int i = 0; i < settings.length; i++) { - copy[i] = settings[i].copy(); - } - return copy; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsStateBlock.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsStateBlock.java deleted file mode 100644 index ce5147b8..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsStateBlock.java +++ /dev/null @@ -1,367 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.persistence; - -import com.github.luben.zstd.Zstd; -import com.hypixel.hytale.codec.Codec; -import com.hypixel.hytale.codec.ExtraInfo; -import com.hypixel.hytale.codec.KeyedCodec; -import com.hypixel.hytale.codec.builder.BuilderCodec; -import com.hypixel.hytale.codec.codecs.array.ArrayCodec; -import com.hypixel.hytale.codec.validation.Validators; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.zip.CRC32; -import javax.annotation.Nonnull; -import lombok.Getter; -import org.bson.BsonBinaryReader; -import org.bson.BsonBinaryWriter; -import org.bson.BsonDocument; -import org.bson.BsonValue; -import org.bson.codecs.BsonDocumentCodec; -import org.bson.codecs.DecoderContext; -import org.bson.codecs.EncoderContext; -import org.bson.io.BasicOutputBuffer; - -/** - * Compressed block for high-cardinality persistent physics state. - */ -public class PersistentPhysicsStateBlock { - - static final String KIND_BODIES = "bodies"; - static final String KIND_JOINTS = "joints"; - static final String CODEC_BSON_ARRAY_V1 = "bson-array-v1"; - static final String COMPRESSION_ZSTD = "zstd"; - private static final String PAYLOAD_ITEMS_KEY = "Items"; - private static final Set SUPPORTED_KINDS = Set.of(KIND_BODIES, KIND_JOINTS); - private static final int ZSTD_COMPRESSION_LEVEL = 3; - private static final int MAX_UNCOMPRESSED_BYTES = 64 * 1024 * 1024; - private static final String UNCOMPRESSED_BYTES_LIMIT_MESSAGE = - "Persistent physics state block uncompressed size exceeds limit"; - private static final byte[] EMPTY_PAYLOAD = new byte[0]; - private static final Codec BINARY_PAYLOAD_CODEC = new PersistentPhysicsBinaryPayloadCodec(); - private static final BsonDocumentCodec BSON_DOCUMENT_CODEC = new BsonDocumentCodec(); - private static final ArrayCodec BODY_ARRAY_CODEC = - new ArrayCodec<>(PersistentPhysicsBodyState.CODEC, PersistentPhysicsBodyState[]::new); - private static final ArrayCodec JOINT_ARRAY_CODEC = - new ArrayCodec<>(PersistentPhysicsJointState.CODEC, PersistentPhysicsJointState[]::new); - - @Nonnull - public static final BuilderCodec CODEC = BuilderCodec.builder( - PersistentPhysicsStateBlock.class, - PersistentPhysicsStateBlock::new) - .append(new KeyedCodec<>("Kind", Codec.STRING, false), - (block, value) -> block.kind = value, - PersistentPhysicsStateBlock::getKind) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.stringIn(SUPPORTED_KINDS, - "Persistent physics state block kind is unsupported")) - .add() - .append(new KeyedCodec<>("Codec", Codec.STRING, false), - (block, value) -> block.codec = value, - PersistentPhysicsStateBlock::getCodec) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.stringEquals(CODEC_BSON_ARRAY_V1, - "Persistent physics state block codec is unsupported")) - .add() - .append(new KeyedCodec<>("Compression", Codec.STRING, false), - (block, value) -> block.compression = value, - PersistentPhysicsStateBlock::getCompression) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.stringEquals(COMPRESSION_ZSTD, - "Persistent physics state block compression is unsupported")) - .add() - .append(new KeyedCodec<>("SchemaVersion", Codec.INTEGER, false), - (block, value) -> block.schemaVersion = value, - PersistentPhysicsStateBlock::getSchemaVersion) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(PersistentPhysicsWorldResource.CURRENT_SCHEMA_VERSION, - PersistentPhysicsWorldResource.CURRENT_SCHEMA_VERSION)) - .add() - .append(new KeyedCodec<>("BlockIndex", Codec.INTEGER, false), - (block, value) -> block.blockIndex = value, - PersistentPhysicsStateBlock::getBlockIndex) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(0, Integer.MAX_VALUE)) - .add() - .append(new KeyedCodec<>("SpaceId", Codec.INTEGER, false), - (block, value) -> block.spaceId = value, - PersistentPhysicsStateBlock::getSpaceId) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, Integer.MAX_VALUE)) - .add() - .append(new KeyedCodec<>("ItemCount", Codec.INTEGER, false), - (block, value) -> block.itemCount = value, - PersistentPhysicsStateBlock::getItemCount) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(0, Integer.MAX_VALUE)) - .add() - .append(new KeyedCodec<>("UncompressedBytes", Codec.INTEGER, false), - (block, value) -> block.uncompressedBytes = value, - PersistentPhysicsStateBlock::getUncompressedBytes) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, Integer.MAX_VALUE)) - .addValidator(PersistentPhysicsValidation.intAtMost(MAX_UNCOMPRESSED_BYTES, - UNCOMPRESSED_BYTES_LIMIT_MESSAGE)) - .add() - .append(new KeyedCodec<>("CompressedBytes", Codec.INTEGER, false), - (block, value) -> block.compressedBytes = value, - PersistentPhysicsStateBlock::getCompressedBytes) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(1, Integer.MAX_VALUE)) - .add() - .append(new KeyedCodec<>("Crc32", Codec.LONG, false), - (block, value) -> block.crc32 = value, - PersistentPhysicsStateBlock::getCrc32) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(0L, 0xffff_ffffL)) - .add() - .append(new KeyedCodec<>("Payload", BINARY_PAYLOAD_CODEC, false), - (block, value) -> block.payload = copyPayload(value), - PersistentPhysicsStateBlock::getPayload) - .addValidator(Validators.nonNull()) - .addValidator(PersistentPhysicsValidation.nonEmptyBytes( - "Persistent physics state block payload cannot be empty")) - .add() - .build(); - - @Nonnull - private String kind = ""; - @Nonnull - private String codec = ""; - @Nonnull - private String compression = ""; - @Getter - private int schemaVersion; - @Getter - private int blockIndex; - @Getter - private int spaceId; - @Getter - private int itemCount; - @Getter - private int uncompressedBytes; - @Getter - private int compressedBytes; - @Getter - private long crc32; - @Nonnull - private byte[] payload = EMPTY_PAYLOAD; - - public PersistentPhysicsStateBlock() { - } - - @Nonnull - static PersistentPhysicsStateBlock[] bodyBlocks(@Nonnull PersistentPhysicsBodyState[] bodies) { - Map> bySpace = new LinkedHashMap<>(); - for (PersistentPhysicsBodyState body : bodies) { - bySpace.computeIfAbsent(body.getSpaceId(), ignored -> new ArrayList<>()).add(body.copy()); - } - - List blocks = new ArrayList<>(bySpace.size()); - int blockIndex = 0; - for (Map.Entry> entry : bySpace.entrySet()) { - PersistentPhysicsBodyState[] states = entry.getValue().toArray(PersistentPhysicsBodyState[]::new); - BsonDocument payloadDocument = new BsonDocument(); - payloadDocument.put(PAYLOAD_ITEMS_KEY, BODY_ARRAY_CODEC.encode(states, new ExtraInfo())); - blocks.add(compressed(KIND_BODIES, blockIndex++, entry.getKey(), states.length, payloadDocument)); - } - return blocks.toArray(PersistentPhysicsStateBlock[]::new); - } - - @Nonnull - static PersistentPhysicsStateBlock[] jointBlocks(@Nonnull PersistentPhysicsJointState[] joints) { - Map> bySpace = new LinkedHashMap<>(); - for (PersistentPhysicsJointState joint : joints) { - bySpace.computeIfAbsent(joint.getSpaceId(), ignored -> new ArrayList<>()).add(joint.copy()); - } - - List blocks = new ArrayList<>(bySpace.size()); - int blockIndex = 0; - for (Map.Entry> entry : bySpace.entrySet()) { - PersistentPhysicsJointState[] states = entry.getValue().toArray(PersistentPhysicsJointState[]::new); - BsonDocument payloadDocument = new BsonDocument(); - payloadDocument.put(PAYLOAD_ITEMS_KEY, JOINT_ARRAY_CODEC.encode(states, new ExtraInfo())); - blocks.add(compressed(KIND_JOINTS, blockIndex++, entry.getKey(), states.length, payloadDocument)); - } - return blocks.toArray(PersistentPhysicsStateBlock[]::new); - } - - @Nonnull - static PersistentPhysicsBodyState[] decodeBodyBlocks(@Nonnull PersistentPhysicsStateBlock[] blocks) { - List states = new ArrayList<>(); - for (PersistentPhysicsStateBlock block : blocks) { - BsonValue items = block.inflate(KIND_BODIES).get(PAYLOAD_ITEMS_KEY); - PersistentPhysicsBodyState[] decoded = BODY_ARRAY_CODEC.decode(items, new ExtraInfo()); - assert decoded != null; - block.requireItemCount(decoded.length); - for (PersistentPhysicsBodyState state : decoded) { - states.add(state.copy()); - } - } - return states.toArray(PersistentPhysicsBodyState[]::new); - } - - @Nonnull - static PersistentPhysicsJointState[] decodeJointBlocks(@Nonnull PersistentPhysicsStateBlock[] blocks) { - List states = new ArrayList<>(); - for (PersistentPhysicsStateBlock block : blocks) { - BsonValue items = block.inflate(KIND_JOINTS).get(PAYLOAD_ITEMS_KEY); - PersistentPhysicsJointState[] decoded = JOINT_ARRAY_CODEC.decode(items, new ExtraInfo()); - assert decoded != null; - block.requireItemCount(decoded.length); - for (PersistentPhysicsJointState state : decoded) { - states.add(state.copy()); - } - } - return states.toArray(PersistentPhysicsJointState[]::new); - } - - @Nonnull - PersistentPhysicsStateBlock copy() { - PersistentPhysicsStateBlock copy = new PersistentPhysicsStateBlock(); - copy.kind = kind; - copy.codec = codec; - copy.compression = compression; - copy.schemaVersion = schemaVersion; - copy.blockIndex = blockIndex; - copy.spaceId = spaceId; - copy.itemCount = itemCount; - copy.uncompressedBytes = uncompressedBytes; - copy.compressedBytes = compressedBytes; - copy.crc32 = crc32; - copy.payload = copyPayload(payload); - return copy; - } - - @Nonnull - public String getKind() { - return kind; - } - - @Nonnull - public String getCodec() { - return codec; - } - - @Nonnull - public String getCompression() { - return compression; - } - - @Nonnull - public byte[] getPayload() { - return copyPayload(payload); - } - - @Nonnull - private static PersistentPhysicsStateBlock compressed(@Nonnull String kind, - int blockIndex, - int spaceId, - int itemCount, - @Nonnull BsonDocument payloadDocument) { - byte[] uncompressed = writeBson(payloadDocument); - requireUncompressedByteLimit(uncompressed.length); - byte[] compressed = Zstd.compress(uncompressed, ZSTD_COMPRESSION_LEVEL); - - PersistentPhysicsStateBlock block = new PersistentPhysicsStateBlock(); - block.kind = kind; - block.codec = CODEC_BSON_ARRAY_V1; - block.compression = COMPRESSION_ZSTD; - block.schemaVersion = PersistentPhysicsWorldResource.CURRENT_SCHEMA_VERSION; - block.blockIndex = blockIndex; - block.spaceId = spaceId; - block.itemCount = itemCount; - block.uncompressedBytes = uncompressed.length; - block.compressedBytes = compressed.length; - block.crc32 = crc32(uncompressed); - block.payload = compressed; - return block; - } - - @Nonnull - private BsonDocument inflate(@Nonnull String expectedKind) { - validateEnvelope(expectedKind); - byte[] uncompressed = Zstd.decompress(payload, uncompressedBytes); - if (uncompressed.length != uncompressedBytes) { - throw new IllegalStateException("Persistent physics state block decompressed to " - + uncompressed.length + " bytes, expected " + uncompressedBytes); - } - long actualCrc32 = crc32(uncompressed); - if (actualCrc32 != crc32) { - throw new IllegalStateException("Persistent physics state block checksum mismatch for " - + kind + " block " + blockIndex); - } - return readBson(uncompressed); - } - - private void validateEnvelope(@Nonnull String expectedKind) { - if (!expectedKind.equals(kind)) { - throw new IllegalStateException("Persistent physics state block kind mismatch: expected " - + expectedKind + ", found " + kind); - } - if (!CODEC_BSON_ARRAY_V1.equals(codec)) { - throw new IllegalStateException("Persistent physics state block codec is unsupported: " + codec); - } - if (!COMPRESSION_ZSTD.equals(compression)) { - throw new IllegalStateException("Persistent physics state block compression is unsupported: " + compression); - } - if (schemaVersion != PersistentPhysicsWorldResource.CURRENT_SCHEMA_VERSION) { - throw new IllegalStateException("Persistent physics state block schema is unsupported: " + schemaVersion); - } - requireUncompressedByteLimit(uncompressedBytes); - if (payload.length != compressedBytes) { - throw new IllegalStateException("Persistent physics state block compressed size mismatch for " - + kind + " block " + blockIndex); - } - } - - private static void requireUncompressedByteLimit(int byteCount) { - if (byteCount > MAX_UNCOMPRESSED_BYTES) { - throw new IllegalStateException(UNCOMPRESSED_BYTES_LIMIT_MESSAGE - + ": " + byteCount + " > " + MAX_UNCOMPRESSED_BYTES); - } - } - - private void requireItemCount(int decodedCount) { - if (decodedCount != itemCount) { - throw new IllegalStateException("Persistent physics state block item count mismatch for " - + kind + " block " + blockIndex + ": expected " + itemCount + ", decoded " + decodedCount); - } - } - - @Nonnull - private static byte[] writeBson(@Nonnull BsonDocument document) { - try (BasicOutputBuffer output = new BasicOutputBuffer()) { - try (BsonBinaryWriter writer = new BsonBinaryWriter(output)) { - BSON_DOCUMENT_CODEC.encode(writer, document, EncoderContext.builder().build()); - } - return output.toByteArray(); - } - } - - @Nonnull - private static BsonDocument readBson(@Nonnull byte[] bytes) { - try (BsonBinaryReader reader = new BsonBinaryReader(ByteBuffer.wrap(bytes))) { - return BSON_DOCUMENT_CODEC.decode(reader, DecoderContext.builder().build()); - } - } - - private static long crc32(@Nonnull byte[] bytes) { - CRC32 crc32 = new CRC32(); - crc32.update(bytes); - return crc32.getValue(); - } - - @Nonnull - private static byte[] copyPayload(byte[] payload) { - if (payload == null || payload.length == 0) { - return EMPTY_PAYLOAD; - } - return Arrays.copyOf(payload, payload.length); - } - -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsValidation.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsValidation.java deleted file mode 100644 index 174a48a7..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsValidation.java +++ /dev/null @@ -1,187 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.persistence; - -import com.hypixel.hytale.codec.schema.SchemaContext; -import com.hypixel.hytale.codec.schema.config.Schema; -import com.hypixel.hytale.codec.validation.Validator; -import com.hypixel.hytale.codec.validation.ValidationResults; -import dev.hytalemodding.impulse.api.ShapeType; -import java.util.Set; -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -final class PersistentPhysicsValidation { - - private PersistentPhysicsValidation() { - } - - @Nonnull - static Validator finiteFloat(@Nonnull String message) { - return new Validator<>() { - @Override - public void accept(Float value, ValidationResults results) { - if (value == null) { - return; - } - if (!Float.isFinite(value)) { - results.fail(message); - } - } - - @Override - public void updateSchema(SchemaContext context, Schema schema) { - } - }; - } - - @Nonnull - static Validator nonNegativeFiniteFloat(@Nonnull String message) { - return new Validator<>() { - @Override - public void accept(Float value, ValidationResults results) { - if (value == null) { - return; - } - if (!Float.isFinite(value) || value < 0.0f) { - results.fail(message); - } - } - - @Override - public void updateSchema(SchemaContext context, Schema schema) { - } - }; - } - - @Nonnull - static Validator intAtMost(int max, @Nonnull String message) { - return new Validator<>() { - @Override - public void accept(Integer value, ValidationResults results) { - if (value == null) { - return; - } - if (value > max) { - results.fail(message + ": " + value + " > " + max); - } - } - - @Override - public void updateSchema(SchemaContext context, Schema schema) { - } - }; - } - - @Nonnull - static Validator finiteVector(@Nonnull String message) { - return new Validator<>() { - @Override - public void accept(Vector3f value, ValidationResults results) { - if (value == null) { - return; - } - if (!Float.isFinite(value.x) || !Float.isFinite(value.y) || !Float.isFinite(value.z)) { - results.fail(message); - } - } - - @Override - public void updateSchema(SchemaContext context, Schema schema) { - } - }; - } - - @Nonnull - static Validator persistentShapeType() { - return new Validator<>() { - @Override - public void accept(ShapeType value, ValidationResults results) { - if (value == null) { - return; - } - if (value == ShapeType.UNKNOWN || value == ShapeType.VOXELS) { - results.fail("Persisted body shape is unsupported: " + value); - } - } - - @Override - public void updateSchema(SchemaContext context, Schema schema) { - } - }; - } - - @Nonnull - static Validator nonBlankString(@Nonnull String message) { - return new Validator<>() { - @Override - public void accept(String value, ValidationResults results) { - if (value == null) { - return; - } - if (value.isBlank()) { - results.fail(message); - } - } - - @Override - public void updateSchema(SchemaContext context, Schema schema) { - } - }; - } - - @Nonnull - static Validator stringEquals(@Nonnull String expected, @Nonnull String message) { - return new Validator<>() { - @Override - public void accept(String value, ValidationResults results) { - if (value == null) { - return; - } - if (!expected.equals(value)) { - results.fail(message); - } - } - - @Override - public void updateSchema(SchemaContext context, Schema schema) { - } - }; - } - - @Nonnull - static Validator stringIn(@Nonnull Set expected, @Nonnull String message) { - return new Validator<>() { - @Override - public void accept(String value, ValidationResults results) { - if (value == null) { - return; - } - if (!expected.contains(value)) { - results.fail(message); - } - } - - @Override - public void updateSchema(SchemaContext context, Schema schema) { - } - }; - } - - @Nonnull - static Validator nonEmptyBytes(@Nonnull String message) { - return new Validator<>() { - @Override - public void accept(byte[] value, ValidationResults results) { - if (value == null) { - return; - } - if (value.length == 0) { - results.fail(message); - } - } - - @Override - public void updateSchema(SchemaContext context, Schema schema) { - } - }; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsWorldResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsWorldResource.java deleted file mode 100644 index 1e67abb3..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsWorldResource.java +++ /dev/null @@ -1,568 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.persistence; - -import com.hypixel.hytale.codec.Codec; -import com.hypixel.hytale.codec.KeyedCodec; -import com.hypixel.hytale.codec.builder.BuilderCodec; -import com.hypixel.hytale.codec.codecs.array.ArrayCodec; -import com.hypixel.hytale.codec.schema.SchemaContext; -import com.hypixel.hytale.codec.schema.config.Schema; -import com.hypixel.hytale.codec.validation.Validator; -import com.hypixel.hytale.codec.validation.ValidationResults; -import com.hypixel.hytale.codec.validation.Validators; -import com.hypixel.hytale.component.ResourceType; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.internal.systems.persistence.PersistentPhysicsWorldSyncSystem; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.persistence.PhysicsPersistenceResource; -import dev.hytalemodding.impulse.core.plugin.persistence.PhysicsPersistenceSyncResult; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsEventCollectionMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepSchedulingMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2IntMap; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -import java.util.Arrays; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Codec-backed world-level physics resource for the persistence layer. - * - *

This resource stores the world-level state that does not belong - * on individual entities: the space definitions (id, backend, gravity, world-collision - * settings), the body states (keyed by stable physics body keys), the joint - * definitions (keyed by endpoint body keys), and world simulation settings.

- * - *

The {@code runtimeRestorePending} flag is set by {@code afterDecode} whenever - * Hytale deserializes this resource. It signals the hydration systems that they - * need to recreate live spaces, bodies, and joints from the persisted data. The - * flag is cleared once restore finishes.

- * - *

Restore semantics are intentionally asymmetric. Missing saved backends are a - * hard failure because the world-level space definitions cannot be recreated at - * all. Missing entities, missing runtime bodies, and unresolved joints are softer - * failures: those entries are skipped, counted, and reported so restore can finish - * instead of hanging forever.

- */ -public class PersistentPhysicsWorldResource extends PhysicsPersistenceResource { - - public static final int CURRENT_SCHEMA_VERSION = PhysicsPersistenceResource.CURRENT_SCHEMA_VERSION; - private static final PersistentPhysicsSpaceState[] EMPTY_SPACES = new PersistentPhysicsSpaceState[0]; - private static final PersistentPhysicsBodyState[] EMPTY_BODIES = new PersistentPhysicsBodyState[0]; - private static final PersistentPhysicsJointState[] EMPTY_JOINTS = new PersistentPhysicsJointState[0]; - private static final PersistentPhysicsStateBlock[] EMPTY_STATE_BLOCKS = new PersistentPhysicsStateBlock[0]; - - @Nonnull - public static final BuilderCodec CODEC = BuilderCodec.builder( - PersistentPhysicsWorldResource.class, - PersistentPhysicsWorldResource::new) - .append(new KeyedCodec<>("SchemaVersion", Codec.INTEGER, false), - PersistentPhysicsWorldResource::setSchemaVersion, - PersistentPhysicsWorldResource::getSchemaVersion) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range(CURRENT_SCHEMA_VERSION, CURRENT_SCHEMA_VERSION)) - .add() - .append(new KeyedCodec<>("SimulationSteps", Codec.INTEGER), - (resource, value) -> resource.worldSettings.setSimulationSteps(value), - resource -> resource.worldSettings.getSimulationSteps()) - .addValidator(Validators.nonNull()) - .addValidator(Validators.range( - PhysicsWorldSettings.MIN_SIMULATION_STEPS, - PhysicsWorldSettings.MAX_SIMULATION_STEPS)) - .add() - .append(new KeyedCodec<>("StepMode", Codec.STRING), - (resource, value) -> resource.worldSettings.setStepMode(PhysicsStepMode.parse(value)), - resource -> resource.worldSettings.getStepMode().getSerializedName()) - .addValidator(Validators.nonNull()) - .addValidator(stepModeName()) - .add() - .append(new KeyedCodec<>("StepSchedulingMode", Codec.STRING), - (resource, value) -> resource.worldSettings.setStepSchedulingMode( - PhysicsStepSchedulingMode.parse(value)), - resource -> resource.worldSettings.getStepSchedulingMode().getSerializedName()) - .addValidator(Validators.nonNull()) - .addValidator(stepSchedulingModeName()) - .add() - .append(new KeyedCodec<>("EventCollectionMode", Codec.STRING, false), - (resource, value) -> resource.worldSettings.setEventCollectionMode( - PhysicsEventCollectionMode.parse(value)), - resource -> resource.worldSettings.getEventCollectionMode().getSerializedName()) - .addValidator(Validators.nonNull()) - .addValidator(eventCollectionModeName()) - .add() - .append(new KeyedCodec<>("MaxStepDt", Codec.FLOAT), - (resource, value) -> resource.worldSettings.setMaxStepDt(value), - resource -> resource.worldSettings.getMaxStepDt()) - .addValidator(Validators.nonNull()) - .addValidator(Validators.greaterThan(0.0f)) - .addValidator(Validators.max(Float.MAX_VALUE)) - .add() - .append(new KeyedCodec<>("Spaces", - new ArrayCodec<>(PersistentPhysicsSpaceState.CODEC, PersistentPhysicsSpaceState[]::new)), - (resource, value) -> resource.spaces = copySpaces(value), - PersistentPhysicsWorldResource::getSpaces) - .addValidator(Validators.nonNull()) - .addValidator(Validators.nonNullArrayElements()) - .add() - .append(new KeyedCodec<>("BodyBlocks", - new ArrayCodec<>(PersistentPhysicsStateBlock.CODEC, PersistentPhysicsStateBlock[]::new)), - (resource, value) -> resource.bodyBlocks = copyStateBlocks(value), - PersistentPhysicsWorldResource::getBodyBlocksForCodec) - .addValidator(Validators.nonNull()) - .addValidator(Validators.nonNullArrayElements()) - .add() - .append(new KeyedCodec<>("JointBlocks", - new ArrayCodec<>(PersistentPhysicsStateBlock.CODEC, PersistentPhysicsStateBlock[]::new)), - (resource, value) -> resource.jointBlocks = copyStateBlocks(value), - PersistentPhysicsWorldResource::getJointBlocksForCodec) - .addValidator(Validators.nonNull()) - .addValidator(Validators.nonNullArrayElements()) - .add() - .afterDecode(PersistentPhysicsWorldResource::afterCodecDecode) - .build(); - - private int schemaVersion = CURRENT_SCHEMA_VERSION; - @Nonnull - private final PhysicsWorldSettings worldSettings = new PhysicsWorldSettings(); - @Nonnull - private PersistentPhysicsSpaceState[] spaces = EMPTY_SPACES; - @Nonnull - private PersistentPhysicsBodyState[] bodies = EMPTY_BODIES; - @Nonnull - private PersistentPhysicsJointState[] joints = EMPTY_JOINTS; - @Nonnull - private PersistentPhysicsStateBlock[] bodyBlocks = EMPTY_STATE_BLOCKS; - @Nonnull - private PersistentPhysicsStateBlock[] jointBlocks = EMPTY_STATE_BLOCKS; - private transient boolean runtimeRestorePending; - private transient boolean runtimeSpaceBootstrapComplete; - private transient boolean runtimeRestoreFailed; - @Nonnull - private transient String runtimeRestoreFailureMessage = ""; - private transient int runtimeRestoredSpaceCount; - private transient int runtimeRestoredBodyCount; - private transient int runtimeRestoredJointCount; - private transient long runtimeRestoreGeneration; - @Nonnull - private final transient Object2IntMap runtimeSkippedBodiesByReason = new Object2IntLinkedOpenHashMap<>(); - @Nonnull - private final transient Set runtimeSkippedBodyKeys = new ObjectOpenHashSet<>(); - @Nonnull - private final transient Object2IntMap runtimeSkippedJointsByReason = new Object2IntLinkedOpenHashMap<>(); - @Nonnull - private final transient Set runtimeSkippedJointKeys = new ObjectOpenHashSet<>(); - private transient boolean runtimeSnapshotSynced; - private transient int runtimeSnapshotSyncSkipTicks; - - public PersistentPhysicsWorldResource() { - } - - @Nonnull - @SuppressWarnings("unchecked") - public static ResourceType getResourceType() { - return (ResourceType) - ImpulsePlugin.get().getPersistentPhysicsWorldResourceType(); - } - - @Override - public int getSchemaVersion() { - return schemaVersion; - } - - public void setSchemaVersion(int schemaVersion) { - if (schemaVersion != CURRENT_SCHEMA_VERSION) { - throw new IllegalArgumentException("Schema version must be " - + CURRENT_SCHEMA_VERSION); - } - this.schemaVersion = schemaVersion; - } - - @Nonnull - @Override - public PhysicsPersistenceSyncResult saveRuntimeSnapshot(@Nonnull Store store, - @Nonnull PhysicsWorldResource runtime) { - PersistentPhysicsWorldSyncSystem.SyncResult result = - PersistentPhysicsWorldSyncSystem.syncRuntimeSnapshot(store, this, runtime); - return new PhysicsPersistenceSyncResult(result.synced(), - result.spaces(), - result.bodies(), - result.joints(), - result.skippedReason()); - } - - @Nonnull - public PhysicsWorldSettings getWorldSettings() { - return new PhysicsWorldSettings(worldSettings); - } - - public void setWorldSettings(@Nonnull PhysicsWorldSettings settings) { - worldSettings.copyFrom(settings); - } - - @Nonnull - public PersistentPhysicsSpaceState[] getSpaces() { - return copySpaces(spaces); - } - - @Override - public int getSpaceCount() { - return spaces.length; - } - - public void setSpaces(@Nonnull PersistentPhysicsSpaceState[] spaces) { - this.spaces = copySpaces(spaces); - } - - @Nonnull - public PersistentPhysicsBodyState[] getBodies() { - return copyBodies(bodies); - } - - @Override - public int getBodyCount() { - return bodies.length; - } - - public void setBodies(@Nonnull PersistentPhysicsBodyState[] bodies) { - this.bodies = copyBodies(bodies); - } - - @Nonnull - public PersistentPhysicsJointState[] getJoints() { - return copyJoints(joints); - } - - @Override - public int getJointCount() { - return joints.length; - } - - public void setJoints(@Nonnull PersistentPhysicsJointState[] joints) { - this.joints = copyJoints(joints); - } - - @Override - public boolean isRuntimeRestorePending() { - return runtimeRestorePending; - } - - @Override - public void markRuntimeRestorePending() { - runtimeRestoreGeneration++; - runtimeRestorePending = true; - runtimeSpaceBootstrapComplete = false; - runtimeRestoreFailed = false; - runtimeRestoreFailureMessage = ""; - runtimeRestoredSpaceCount = 0; - runtimeRestoredBodyCount = 0; - runtimeRestoredJointCount = 0; - runtimeSnapshotSynced = false; - runtimeSnapshotSyncSkipTicks = 0; - runtimeSkippedBodiesByReason.clear(); - runtimeSkippedBodyKeys.clear(); - runtimeSkippedJointsByReason.clear(); - runtimeSkippedJointKeys.clear(); - } - - @Override - public boolean isRuntimeSpaceBootstrapComplete() { - return runtimeSpaceBootstrapComplete; - } - - public void clearRuntimeRestorePending() { - runtimeRestoreGeneration++; - runtimeRestorePending = false; - runtimeSnapshotSynced = false; - runtimeSnapshotSyncSkipTicks = 0; - } - - public long runtimeRestoreGeneration() { - return runtimeRestoreGeneration; - } - - public boolean shouldSyncRuntimeSnapshot(int intervalTicks) { - if (!runtimeSnapshotSynced || intervalTicks <= 0) { - return true; - } - if (runtimeSnapshotSyncSkipTicks < intervalTicks) { - runtimeSnapshotSyncSkipTicks++; - return false; - } - return true; - } - - public void markRuntimeSnapshotSynced() { - runtimeSnapshotSynced = true; - runtimeSnapshotSyncSkipTicks = 0; - } - - public void markRuntimeSpaceBootstrapComplete(int restoredSpaceCount) { - runtimeSpaceBootstrapComplete = true; - runtimeRestoredSpaceCount = restoredSpaceCount; - } - - @Override - public boolean hasRuntimeRestoreFailed() { - return runtimeRestoreFailed; - } - - public void failRuntimeRestore(@Nonnull String message) { - runtimeRestorePending = false; - runtimeRestoreFailed = true; - runtimeRestoreFailureMessage = message; - } - - public void recordRuntimeBodyRestored() { - runtimeRestoredBodyCount++; - } - - public void recordRuntimeBodySkipped(@Nonnull String reason) { - runtimeSkippedBodiesByReason.put(reason, runtimeSkippedBodiesByReason.getInt(reason) + 1); - } - - public void recordRuntimeBodySkipped(@Nonnull RigidBodyKey bodyKey, @Nonnull String reason) { - runtimeSkippedBodyKeys.add(bodyKey); - recordRuntimeBodySkipped(reason); - } - - public boolean isRuntimeBodySkipped(@Nonnull RigidBodyKey bodyKey) { - return runtimeSkippedBodyKeys.contains(bodyKey); - } - - public void recordRuntimeJointRestored() { - runtimeRestoredJointCount++; - } - - public void recordRuntimeJointSkipped(@Nonnull String key, @Nonnull String reason) { - if (!runtimeSkippedJointKeys.add(key)) { - return; - } - runtimeSkippedJointsByReason.put(reason, runtimeSkippedJointsByReason.getInt(reason) + 1); - } - - @Override - public boolean hasRuntimeRestoreSkips() { - return !runtimeSkippedBodiesByReason.isEmpty() || !runtimeSkippedJointsByReason.isEmpty(); - } - - @Nonnull - @Override - public String runtimeRestoreSummary() { - return "Impulse persistence restore completed: " - + runtimeRestoredSpaceCount + " spaces, " - + runtimeRestoredBodyCount + " bodies restored, " - + countReasons(runtimeSkippedBodiesByReason) + " bodies skipped (" - + formatReasons(runtimeSkippedBodiesByReason) + "), " - + runtimeRestoredJointCount + " joints restored, " - + countReasons(runtimeSkippedJointsByReason) + " joints skipped (" - + formatReasons(runtimeSkippedJointsByReason) + ")."; - } - - @Nonnull - @Override - public String runtimeRestoreFailureSummary() { - return "Impulse persistence restore failed: " + runtimeRestoreFailureMessage + " " - + "Partial progress before failure: " - + runtimeRestoredSpaceCount + " spaces, " - + runtimeRestoredBodyCount + " bodies restored, " - + countReasons(runtimeSkippedBodiesByReason) + " bodies skipped, " - + runtimeRestoredJointCount + " joints restored, " - + countReasons(runtimeSkippedJointsByReason) + " joints skipped."; - } - - public void copyFrom(@Nonnull PersistentPhysicsWorldResource other) { - if (this == other) { - return; - } - setSchemaVersion(other.schemaVersion); - worldSettings.copyFrom(other.worldSettings); - spaces = copySpaces(other.spaces); - bodies = copyBodies(other.bodies); - joints = copyJoints(other.joints); - bodyBlocks = EMPTY_STATE_BLOCKS; - jointBlocks = EMPTY_STATE_BLOCKS; - runtimeRestorePending = false; - runtimeSpaceBootstrapComplete = false; - runtimeRestoreFailed = false; - runtimeRestoreFailureMessage = ""; - runtimeRestoredSpaceCount = 0; - runtimeRestoredBodyCount = 0; - runtimeRestoredJointCount = 0; - runtimeRestoreGeneration = 0L; - runtimeSnapshotSynced = false; - runtimeSnapshotSyncSkipTicks = 0; - runtimeSkippedBodiesByReason.clear(); - runtimeSkippedBodyKeys.clear(); - runtimeSkippedJointsByReason.clear(); - runtimeSkippedJointKeys.clear(); - } - - private void afterCodecDecode() { - if (bodyBlocks.length > 0) { - bodies = PersistentPhysicsStateBlock.decodeBodyBlocks(bodyBlocks); - } - if (jointBlocks.length > 0) { - joints = PersistentPhysicsStateBlock.decodeJointBlocks(jointBlocks); - } - bodyBlocks = EMPTY_STATE_BLOCKS; - jointBlocks = EMPTY_STATE_BLOCKS; - markRuntimeRestorePending(); - } - - @Nonnull - private PersistentPhysicsStateBlock[] getBodyBlocksForCodec() { - if (bodies.length == 0) { - return EMPTY_STATE_BLOCKS; - } - return PersistentPhysicsStateBlock.bodyBlocks(bodies); - } - - @Nonnull - private PersistentPhysicsStateBlock[] getJointBlocksForCodec() { - if (joints.length == 0) { - return EMPTY_STATE_BLOCKS; - } - return PersistentPhysicsStateBlock.jointBlocks(joints); - } - - @Nonnull - private static PersistentPhysicsBodyState[] copyBodies(@Nonnull PersistentPhysicsBodyState[] source) { - PersistentPhysicsBodyState[] copy = Arrays.copyOf(source, source.length); - for (int i = 0; i < copy.length; i++) { - copy[i] = copy[i].copy(); - } - return copy; - } - - @Nonnull - @Override - public PersistentPhysicsWorldResource clone() { - PersistentPhysicsWorldResource copy = new PersistentPhysicsWorldResource(); - copy.copyFrom(this); - return copy; - } - - @Nonnull - private static PersistentPhysicsSpaceState[] copySpaces(@Nonnull PersistentPhysicsSpaceState[] source) { - PersistentPhysicsSpaceState[] copy = Arrays.copyOf(source, source.length); - for (int i = 0; i < copy.length; i++) { - copy[i] = copy[i].copy(); - } - return copy; - } - - @Nonnull - private static PersistentPhysicsJointState[] copyJoints(@Nonnull PersistentPhysicsJointState[] source) { - PersistentPhysicsJointState[] copy = Arrays.copyOf(source, source.length); - for (int i = 0; i < copy.length; i++) { - copy[i] = copy[i].copy(); - } - return copy; - } - - @Nonnull - private static PersistentPhysicsStateBlock[] copyStateBlocks(@Nullable PersistentPhysicsStateBlock[] source) { - if (source == null || source.length == 0) { - return EMPTY_STATE_BLOCKS; - } - PersistentPhysicsStateBlock[] copy = Arrays.copyOf(source, source.length); - for (int i = 0; i < copy.length; i++) { - copy[i] = copy[i].copy(); - } - return copy; - } - - @Nonnull - private static Validator stepModeName() { - return new Validator<>() { - @Override - public void accept(String value, ValidationResults results) { - if (value == null) { - return; - } - try { - PhysicsStepMode.parse(value); - } catch (IllegalArgumentException exception) { - results.fail("Persistent physics step mode is unknown: " + value); - } - } - - @Override - public void updateSchema(SchemaContext context, Schema schema) { - } - }; - } - - @Nonnull - private static Validator stepSchedulingModeName() { - return new Validator<>() { - @Override - public void accept(String value, ValidationResults results) { - if (value == null) { - return; - } - try { - PhysicsStepSchedulingMode.parse(value); - } catch (IllegalArgumentException exception) { - results.fail("Persistent physics step scheduling mode is unknown: " + value); - } - } - - @Override - public void updateSchema(SchemaContext context, Schema schema) { - } - }; - } - - @Nonnull - private static Validator eventCollectionModeName() { - return new Validator<>() { - @Override - public void accept(String value, ValidationResults results) { - if (value == null) { - return; - } - try { - PhysicsEventCollectionMode.parse(value); - } catch (IllegalArgumentException exception) { - results.fail("Persistent physics event collection mode is unknown: " + value); - } - } - - @Override - public void updateSchema(SchemaContext context, Schema schema) { - } - }; - } - - private static int countReasons(@Nonnull Object2IntMap reasons) { - int total = 0; - for (int count : reasons.values()) { - total += count; - } - return total; - } - - @Nonnull - private static String formatReasons(@Nonnull Object2IntMap reasons) { - if (reasons.isEmpty()) { - return "none"; - } - - StringBuilder builder = new StringBuilder(); - boolean first = true; - for (Object2IntMap.Entry entry : reasons.object2IntEntrySet()) { - if (!first) { - builder.append(", "); - } - builder.append(entry.getKey()).append('=').append(entry.getIntValue()); - first = false; - } - return builder.toString(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PhysicsStoreHolderPersistence.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PhysicsStoreHolderPersistence.java new file mode 100644 index 00000000..1959f4fd --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PhysicsStoreHolderPersistence.java @@ -0,0 +1,315 @@ +package dev.hytalemodding.impulse.core.internal.persistence; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.util.ChunkUtil; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.BsonUtil; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionRestoreDependencyComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.plugin.components.BodyCommandComponent; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.ChunkCollisionSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.bson.BsonDocument; +import org.joml.Vector3f; + +/** + * ChunkStore-shaped holder serialization boundary for future PhysicsStore row storage. + */ +public final class PhysicsStoreHolderPersistence { + + private static final UUID NIL_UUID = new UUID(0L, 0L); + + private PhysicsStoreHolderPersistence() { + } + + @Nonnull + public static List> capturePersistentHolders( + @Nonnull Store store) { + Capture capture = new Capture(store, snapshotBodiesByUuid(store)); + BiConsumer, CommandBuffer> collector = + (chunk, _) -> capture.collectChunk(chunk); + store.forEachChunk(UuidComponent.getComponentType(), collector); + return capture.toHolders(); + } + + @Nonnull + public static List capturePersistentHolderBlobs( + @Nonnull Store store) { + return capturePersistentHolders(store).stream() + .map(holder -> encodeHolder(store, holder)) + .toList(); + } + + @Nonnull + public static byte[] encodeHolder(@Nonnull Store store, + @Nonnull Holder holder) { + BsonDocument document = store.getRegistry().serialize( + Objects.requireNonNull(holder, "holder")); + return BsonUtil.writeToBytes(document); + } + + @Nonnull + public static Holder decodeHolder( + @Nonnull ComponentRegistry registry, + @Nonnull byte[] bytes) { + BsonDocument document = BsonUtil.readFromBytes(Objects.requireNonNull(bytes, "bytes")); + Holder holder = registry.deserialize(document); + if (holder == null) { + throw new IllegalArgumentException("PhysicsStore holder payload decoded to null"); + } + return holder; + } + + @Nonnull + private static Map snapshotBodiesByUuid( + @Nonnull Store store) { + PhysicsSnapshotResource snapshots = store.getResource(PhysicsSnapshotResource.getResourceType()); + Map bodies = new Object2ObjectOpenHashMap<>(); + for (PhysicsBodySnapshot body : snapshots.getLatestFrame().bodies()) { + bodies.put(body.bodyUuid(), body); + } + return bodies; + } + + private static final class Capture { + + @Nonnull + private final Store store; + @Nonnull + private final Map snapshotsByBodyUuid; + @Nonnull + private final List spaces = new ArrayList<>(); + @Nonnull + private final List bodies = new ArrayList<>(); + @Nonnull + private final List joints = new ArrayList<>(); + @Nonnull + private final ObjectOpenHashSet persistentBodyUuids = new ObjectOpenHashSet<>(); + @Nonnull + private final Map> generatedSourcesBySpace = + new Object2ObjectOpenHashMap<>(); + @Nonnull + private final Map chunkSettingsBySpace = + new Object2ObjectOpenHashMap<>(); + + private Capture(@Nonnull Store store, + @Nonnull Map snapshotsByBodyUuid) { + this.store = store; + this.snapshotsByBodyUuid = snapshotsByBodyUuid; + } + + private void collectChunk(@Nonnull ArchetypeChunk chunk) { + for (int index = 0; index < chunk.size(); index++) { + UuidComponent uuidComponent = chunk.getComponent(index, + UuidComponent.getComponentType()); + if (uuidComponent == null || NIL_UUID.equals(uuidComponent.getUuid())) { + continue; + } + UUID uuid = uuidComponent.getUuid(); + Ref ref = chunk.getReferenceTo(index); + if (chunk.getComponent(index, SpaceComponent.getComponentType()) != null) { + spaces.add(new Row(uuid, ref)); + ChunkCollisionSettingsComponent settings = chunk.getComponent(index, + ChunkCollisionSettingsComponent.getComponentType()); + if (settings != null) { + chunkSettingsBySpace.put(uuid, settings); + } + } + + BodyComponent body = chunk.getComponent(index, BodyComponent.getComponentType()); + ChunkCollisionSourceComponent source = chunk.getComponent(index, + ChunkCollisionSourceComponent.getComponentType()); + if (body != null && source != null) { + generatedSourcesBySpace + .computeIfAbsent(body.getSpaceUuid(), _ -> new ArrayList<>()) + .add(source); + } + if (body != null && shouldPersistBody(chunk, index, body)) { + bodies.add(new Row(uuid, ref)); + persistentBodyUuids.add(uuid); + } + + JointComponent joint = chunk.getComponent(index, JointComponent.getComponentType()); + if (joint != null) { + joints.add(new Row(uuid, ref, joint)); + } + } + } + + @Nonnull + private List> toHolders() { + List rows = new ArrayList<>(spaces.size() + bodies.size() + joints.size()); + rows.addAll(spaces); + rows.addAll(bodies); + for (Row joint : joints) { + if (persistentBodyUuids.contains(joint.requiredJoint().getBodyAUuid()) + && persistentBodyUuids.contains(joint.requiredJoint().getBodyBUuid())) { + rows.add(joint); + } + } + rows.sort(Comparator.comparing(Row::uuid)); + + List> holders = new ArrayList<>(rows.size()); + for (Row row : rows) { + Holder holder = store.copySerializableEntity(row.ref()); + sanitize(holder); + patchBodyTarget(row.uuid(), holder); + attachChunkCollisionRestoreDependency(holder); + holders.add(holder); + } + return List.copyOf(holders); + } + + private void sanitize(@Nonnull Holder holder) { + holder.tryRemoveComponent(BodyCommandComponent.getComponentType()); + holder.tryRemoveComponent(ChunkCollisionSourceComponent.getComponentType()); + } + + private void patchBodyTarget(@Nonnull UUID rowUuid, + @Nonnull Holder holder) { + if (holder.getComponent(BodyComponent.getComponentType()) == null) { + return; + } + + TargetComponent target = holder.getComponent(TargetComponent.getComponentType()); + if (target == null) { + target = new TargetComponent(); + } + PhysicsBodySnapshot snapshot = snapshotsByBodyUuid.get(rowUuid); + if (snapshot != null) { + target.setPosition(snapshot.position()); + target.setRotation(snapshot.rotation()); + target.setLinearVelocity(snapshot.linearVelocity()); + target.setAngularVelocity(snapshot.angularVelocity()); + target.setActivate(!snapshot.sleeping()); + } else { + target.setActivate(target.isActive() || target.isActivate()); + } + target.setActive(false); + target.setTransformEnabled(true); + target.setVelocityEnabled(true); + holder.putComponent(TargetComponent.getComponentType(), target); + } + + private void attachChunkCollisionRestoreDependency( + @Nonnull Holder holder) { + BodyComponent body = holder.getComponent(BodyComponent.getComponentType()); + if (body == null) { + return; + } + DynamicsComponent dynamics = holder.getComponent(DynamicsComponent.getComponentType()); + if (dynamics == null || dynamics.getBodyType() != PhysicsBodyType.DYNAMIC) { + return; + } + ChunkCollisionSettingsComponent settings = chunkSettingsBySpace.get(body.getSpaceUuid()); + if (settings == null || settings.getMode() == PhysicsChunkCollisionMode.NONE) { + return; + } + TargetComponent target = holder.getComponent(TargetComponent.getComponentType()); + if (target == null) { + return; + } + int radius = Math.max(0, settings.getBodyRadius()); + if (!intersectsGeneratedChunkCollision(body.getSpaceUuid(), + target.getPosition(), + radius)) { + return; + } + holder.putComponent(ChunkCollisionRestoreDependencyComponent.getComponentType(), + new ChunkCollisionRestoreDependencyComponent(body.getSpaceUuid(), + target.getPosition(), + radius, + settings.getMode())); + } + + private boolean intersectsGeneratedChunkCollision(@Nonnull UUID spaceUuid, + @Nonnull Vector3f center, + int radius) { + List sources = generatedSourcesBySpace.get(spaceUuid); + if (sources == null || sources.isEmpty()) { + return false; + } + for (ChunkCollisionSourceComponent source : sources) { + if (intersectsSource(source, center, radius)) { + return true; + } + } + return false; + } + + private static boolean intersectsSource(@Nonnull ChunkCollisionSourceComponent source, + @Nonnull Vector3f center, + int radius) { + int minX = (int) Math.floor(center.x) - radius; + int maxX = (int) Math.floor(center.x) + radius; + int minY = Math.max(0, (int) Math.floor(center.y) - radius); + int maxY = Math.min(ChunkUtil.HEIGHT_MINUS_1, + (int) Math.floor(center.y) + radius); + int minZ = (int) Math.floor(center.z) - radius; + int maxZ = (int) Math.floor(center.z) + radius; + + return source.getChunkX() >= ChunkUtil.chunkCoordinate(minX) + && source.getChunkX() <= ChunkUtil.chunkCoordinate(maxX) + && source.getSectionY() >= ChunkUtil.indexSection(minY) + && source.getSectionY() <= ChunkUtil.indexSection(maxY) + && source.getChunkZ() >= ChunkUtil.chunkCoordinate(minZ) + && source.getChunkZ() <= ChunkUtil.chunkCoordinate(maxZ); + } + + private static boolean shouldPersistBody(@Nonnull ArchetypeChunk chunk, + int index, + @Nonnull BodyComponent body) { + return chunk.getComponent(index, ChunkCollisionSourceComponent.getComponentType()) == null + && chunk.getComponent(index, ColliderComponent.getComponentType()) != null + && chunk.getComponent(index, ShapeComponent.getComponentType()) != null + && chunk.getComponent(index, MaterialComponent.getComponentType()) != null + && chunk.getComponent(index, CollisionFilterComponent.getComponentType()) != null; + } + } + + private record Row(@Nonnull UUID uuid, + @Nonnull Ref ref, + @Nullable JointComponent jointComponent) { + + private Row(@Nonnull UUID uuid, @Nonnull Ref ref) { + this(uuid, ref, null); + } + + @Nonnull + private JointComponent requiredJoint() { + if (jointComponent == null) { + throw new IllegalStateException("Row is not a joint"); + } + return jointComponent; + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PhysicsStoreHolderPreflight.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PhysicsStoreHolderPreflight.java new file mode 100644 index 00000000..c8b15007 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PhysicsStoreHolderPreflight.java @@ -0,0 +1,121 @@ +package dev.hytalemodding.impulse.core.internal.persistence; + +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import javax.annotation.Nonnull; + +/** + * Validates decoded holder rows before mutating the live PhysicsStore. + */ +final class PhysicsStoreHolderPreflight { + + private static final UUID NIL_UUID = new UUID(0L, 0L); + + private PhysicsStoreHolderPreflight() { + } + + @Nonnull + static Result validate(@Nonnull List> holders) { + List errors = new ArrayList<>(); + ObjectOpenHashSet seen = new ObjectOpenHashSet<>(); + ObjectOpenHashSet spaces = new ObjectOpenHashSet<>(); + ObjectOpenHashSet bodies = new ObjectOpenHashSet<>(); + List bodyRows = new ArrayList<>(); + List jointRows = new ArrayList<>(); + + for (Holder holder : holders) { + UuidComponent uuidComponent = holder.getComponent(UuidComponent.getComponentType()); + if (uuidComponent == null || NIL_UUID.equals(uuidComponent.getUuid())) { + errors.add("PhysicsStore holder row is missing a durable UUID"); + continue; + } + UUID uuid = uuidComponent.getUuid(); + if (!seen.add(uuid)) { + errors.add("Duplicate PhysicsStore holder UUID: " + uuid); + } + + SpaceComponent space = holder.getComponent(SpaceComponent.getComponentType()); + BodyComponent body = holder.getComponent(BodyComponent.getComponentType()); + JointComponent joint = holder.getComponent(JointComponent.getComponentType()); + int primaryComponents = (space != null ? 1 : 0) + + (body != null ? 1 : 0) + + (joint != null ? 1 : 0); + if (primaryComponents == 0) { + errors.add("PhysicsStore holder " + uuid + + " has no space, body, or joint component"); + continue; + } + if (primaryComponents > 1) { + errors.add("PhysicsStore holder " + uuid + + " mixes space, body, and joint components"); + continue; + } + if (space != null) { + spaces.add(uuid); + } else if (body != null) { + bodies.add(uuid); + bodyRows.add(new BodyRow(uuid, body.getSpaceUuid())); + } else { + jointRows.add(new JointRow(uuid, + joint.getSpaceUuid(), + joint.getBodyAUuid(), + joint.getBodyBUuid())); + } + } + + validateBodyRows(bodyRows, spaces, errors); + validateJointRows(jointRows, spaces, bodies, errors); + return new Result(errors.isEmpty(), List.copyOf(errors)); + } + + private static void validateBodyRows(@Nonnull List bodyRows, + @Nonnull ObjectOpenHashSet spaces, + @Nonnull List errors) { + for (BodyRow body : bodyRows) { + if (NIL_UUID.equals(body.spaceUuid()) || !spaces.contains(body.spaceUuid())) { + errors.add("PhysicsStore holder body " + body.uuid() + + " references missing space " + body.spaceUuid()); + } + } + } + + private static void validateJointRows(@Nonnull List jointRows, + @Nonnull ObjectOpenHashSet spaces, + @Nonnull ObjectOpenHashSet bodies, + @Nonnull List errors) { + for (JointRow joint : jointRows) { + if (NIL_UUID.equals(joint.spaceUuid()) || !spaces.contains(joint.spaceUuid())) { + errors.add("PhysicsStore holder joint " + joint.uuid() + + " references missing space " + joint.spaceUuid()); + } + if (NIL_UUID.equals(joint.bodyAUuid()) || !bodies.contains(joint.bodyAUuid())) { + errors.add("PhysicsStore holder joint " + joint.uuid() + + " references missing body A " + joint.bodyAUuid()); + } + if (NIL_UUID.equals(joint.bodyBUuid()) || !bodies.contains(joint.bodyBUuid())) { + errors.add("PhysicsStore holder joint " + joint.uuid() + + " references missing body B " + joint.bodyBUuid()); + } + } + } + + record Result(boolean valid, @Nonnull List errors) { + } + + private record BodyRow(@Nonnull UUID uuid, @Nonnull UUID spaceUuid) { + } + + private record JointRow(@Nonnull UUID uuid, + @Nonnull UUID spaceUuid, + @Nonnull UUID bodyAUuid, + @Nonnull UUID bodyBUuid) { + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PhysicsStoreHolderStorage.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PhysicsStoreHolderStorage.java new file mode 100644 index 00000000..e30a7785 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/persistence/PhysicsStoreHolderStorage.java @@ -0,0 +1,204 @@ +package dev.hytalemodding.impulse.core.internal.persistence; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.BsonUtil; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.bson.BsonArray; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonInt32; +import org.bson.BsonValue; + +/** + * BSON holder file storage for row-native PhysicsStore persistence. + */ +public final class PhysicsStoreHolderStorage { + + public static final int SCHEMA_VERSION = 1; + private static final String DIRECTORY = "physicsstore"; + private static final String FILE_NAME = "holders.bson"; + private static final String SCHEMA_VERSION_FIELD = "SchemaVersion"; + private static final String HOLDERS_FIELD = "Holders"; + + private PhysicsStoreHolderStorage() { + } + + @Nonnull + public static CompletableFuture save(@Nonnull PhysicsStore physicsStore) { + return save(physicsStore.getStore()); + } + + @Nonnull + public static CompletableFuture save(@Nonnull Store store) { + List holderBlobs = PhysicsStoreHolderPersistence.capturePersistentHolderBlobs( + store); + byte[] document = BsonUtil.writeToBytes(document(holderBlobs)); + Path file = file(store.getExternalData()); + return CompletableFuture.runAsync(() -> writeBinaryAtomic(file, document)); + } + + @Nonnull + public static LoadResult load(@Nonnull Store store) { + Path file = file(store.getExternalData()); + if (!Files.exists(file)) { + return LoadResult.missing(); + } + + BsonDocument document; + try { + document = BsonUtil.readFromBytes(Files.readAllBytes(file)); + } catch (IOException exception) { + throw new IllegalStateException("Could not read PhysicsStore holder storage: " + file, + exception); + } + if (document == null) { + throw new IllegalStateException("PhysicsStore holder storage is empty: " + file); + } + int schemaVersion = document.getInt32(SCHEMA_VERSION_FIELD, new BsonInt32(0)).getValue(); + if (schemaVersion != SCHEMA_VERSION) { + throw new IllegalStateException("Unsupported PhysicsStore holder storage schema " + + schemaVersion + "; expected " + SCHEMA_VERSION); + } + + BsonArray holders = document.getArray(HOLDERS_FIELD, new BsonArray()); + List> decodedHolders = new ArrayList<>(holders.size()); + for (BsonValue value : holders) { + decodedHolders.add(PhysicsStoreHolderPersistence.decodeHolder( + store.getRegistry(), + value.asBinary().getData())); + } + PhysicsStoreHolderPreflight.Result preflight = PhysicsStoreHolderPreflight.validate( + decodedHolders); + if (!preflight.valid()) { + throw new IllegalStateException(String.join("; ", preflight.errors())); + } + + int loaded = 0; + for (Holder holder : decodedHolders) { + store.addEntity(holder, AddReason.LOAD); + loaded++; + } + return new LoadResult(true, loaded); + } + + @Nonnull + public static Summary summary(@Nonnull Store store) { + Path file = file(store.getExternalData()); + if (!Files.exists(file)) { + return Summary.missing(); + } + BsonDocument document; + try { + document = BsonUtil.readFromBytes(Files.readAllBytes(file)); + } catch (IOException exception) { + throw new IllegalStateException("Could not read PhysicsStore holder storage: " + file, + exception); + } + if (document == null) { + return Summary.missing(); + } + int schemaVersion = document.getInt32(SCHEMA_VERSION_FIELD, new BsonInt32(0)).getValue(); + if (schemaVersion != SCHEMA_VERSION) { + throw new IllegalStateException("Unsupported PhysicsStore holder storage schema " + + schemaVersion + "; expected " + SCHEMA_VERSION); + } + BsonArray holders = document.getArray(HOLDERS_FIELD, new BsonArray()); + int spaces = 0; + int bodies = 0; + int joints = 0; + for (BsonValue value : holders) { + Holder holder = PhysicsStoreHolderPersistence.decodeHolder( + store.getRegistry(), + value.asBinary().getData()); + if (holder.getComponent(SpaceComponent.getComponentType()) != null) { + spaces++; + } + if (holder.getComponent(BodyComponent.getComponentType()) != null) { + bodies++; + } + if (holder.getComponent(JointComponent.getComponentType()) != null) { + joints++; + } + } + return new Summary(true, spaces, bodies, joints); + } + + @Nonnull + static Path file(@Nonnull PhysicsStore physicsStore) { + Path savePath = physicsStore.getWorld().getSavePath(); + return primaryFile(savePath); + } + + @Nonnull + private static Path primaryFile(@Nonnull Path savePath) { + return savePath.resolve(DIRECTORY).resolve(FILE_NAME); + } + + @Nonnull + private static BsonDocument document(@Nonnull List holderBlobs) { + BsonArray holders = new BsonArray(); + for (byte[] holderBlob : holderBlobs) { + holders.add(new BsonBinary(holderBlob)); + } + return new BsonDocument() + .append(SCHEMA_VERSION_FIELD, new BsonInt32(SCHEMA_VERSION)) + .append(HOLDERS_FIELD, holders); + } + + private static void writeBinaryAtomic(@Nonnull Path file, @Nonnull byte[] bytes) { + try { + Path parent = file.getParent(); + if (parent != null && !Files.exists(parent)) { + Files.createDirectories(parent); + } + Path temp = file.resolveSibling(file.getFileName() + ".tmp"); + Files.write(temp, + bytes, + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE); + try { + Files.move(temp, + file, + StandardCopyOption.REPLACE_EXISTING, + StandardCopyOption.ATOMIC_MOVE); + } catch (IOException ignored) { + Files.move(temp, file, StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException exception) { + throw new IllegalStateException("Could not write PhysicsStore holder storage: " + file, + exception); + } + } + + public record LoadResult(boolean present, int loadedCount) { + + @Nonnull + private static LoadResult missing() { + return new LoadResult(false, 0); + } + } + + public record Summary(boolean present, int spaces, int bodies, int joints) { + + @Nonnull + private static Summary missing() { + return new Summary(false, 0, 0, 0); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsAsyncCompletions.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsAsyncCompletions.java new file mode 100644 index 00000000..cc5cfc7b --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsAsyncCompletions.java @@ -0,0 +1,32 @@ +package dev.hytalemodding.impulse.core.internal.physics; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ThreadFactory; +import javax.annotation.Nonnull; + +/** + * Completes public PhysicsStore futures outside the store tick lane. + */ +public final class PhysicsAsyncCompletions { + + private static final ThreadFactory COMPLETION_THREADS = + Thread.ofVirtual().name("Impulse PhysicsStore completion ", 1).factory(); + + private PhysicsAsyncCompletions() { + } + + public static void complete(@Nonnull CompletableFuture completion, T value) { + dispatch(() -> completion.complete(value)); + } + + public static void fail(@Nonnull CompletableFuture completion, + @Nonnull Throwable failure) { + Objects.requireNonNull(failure, "failure"); + dispatch(() -> completion.completeExceptionally(failure)); + } + + private static void dispatch(@Nonnull Runnable completion) { + COMPLETION_THREADS.newThread(Objects.requireNonNull(completion, "completion")).start(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsSpaceMutations.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsSpaceMutations.java new file mode 100644 index 00000000..2838c018 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsSpaceMutations.java @@ -0,0 +1,348 @@ +package dev.hytalemodding.impulse.core.internal.physics; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.RemoveReason; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.plugin.components.ExtensionSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.components.SolverSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.VisualMaterializationSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.VisualSyncSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.ChunkCollisionSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.CollisionLodSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsChunkCollisionSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsCollisionLodSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualMaterializationSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualSyncSettings; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsExtensionSettings; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSolverSettings; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Vector3f; + +/** + * Direct PhysicsStore space entity mutations for store-lane callers. + */ +public final class PhysicsSpaceMutations { + + private PhysicsSpaceMutations() { + } + + @Nonnull + public static Ref addSpace(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull SpaceId compatibilitySpaceId, + @Nonnull BackendId backendId) { + PhysicsThreading.requireWorldThread(store, "add a PhysicsStore space entity"); + if (backendId.value().isBlank()) { + throw new IllegalArgumentException("PhysicsStore space backend id is blank: " + + spaceUuid); + } + PhysicsSpaceCompatibilityIndexResource compatibility = store.getResource( + PhysicsSpaceCompatibilityIndexResource.getResourceType()); + if (compatibility.getSpaceUuid(compatibilitySpaceId) != null) { + throw new IllegalArgumentException("PhysicsStore space id=" + + compatibilitySpaceId.value() + " is already registered"); + } + Ref existing = store.getExternalData().getRefFromUUID(spaceUuid); + if (existing != null && existing.isValid()) { + throw new IllegalArgumentException("PhysicsStore space uuid=" + spaceUuid + + " is already registered"); + } + Holder holder = PhysicsEntities.spaceHolder(store, + spaceUuid, + new SpaceComponent(backendId, new Vector3f(0.0f, -9.81f, 0.0f))); + Ref ref = store.addEntity(holder, AddReason.SPAWN); + assert ref != null; + store.getExternalData().putRefForUUID(spaceUuid, ref); + compatibility.putSpace(compatibilitySpaceId, spaceUuid); + SpaceId.reserveAtLeast(compatibilitySpaceId.value()); + store.getResource(PhysicsRuntimeResource.getResourceType()) + .markSpaceSettingsPending(ref); + return ref; + } + + public static void putSpaceGravity(@Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull Vector3f gravity) { + UUID spaceUuid = requireSpaceUuid(store, spaceId); + Ref ref = requireSpaceRef(store, spaceUuid); + putSpaceGravity(store, ref, spaceUuid, gravity); + } + + public static void putSpaceGravity(@Nonnull Store store, + @Nonnull Ref ref, + @Nonnull UUID spaceUuid, + @Nonnull Vector3f gravity) { + PhysicsThreading.requireWorldThread(store, "update PhysicsStore space gravity"); + SpaceComponent space = store.getComponent(ref, SpaceComponent.getComponentType()); + if (space == null) { + throw new IllegalArgumentException("PhysicsStore space uuid=" + spaceUuid + + " entity has no SpaceComponent"); + } + SpaceComponent updated = space.clone(); + updated.setGravity(gravity); + store.putComponent(ref, SpaceComponent.getComponentType(), updated); + store.getResource(PhysicsRuntimeResource.getResourceType()) + .markSpaceSettingsPending(ref); + } + + public static void putChunkCollisionSettings(@Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull PhysicsChunkCollisionSettings settings) { + UUID spaceUuid = requireSpaceUuid(store, spaceId); + Ref ref = requireSpaceRef(store, spaceUuid); + putChunkCollisionSettings(store, ref, settings); + } + + public static void putChunkCollisionSettings(@Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PhysicsChunkCollisionSettings settings) { + requireSpaceUuid(store, ref); + PhysicsThreading.requireWorldThread(store, + "update PhysicsStore chunk collision settings"); + ChunkCollisionSettingsComponent component = + new ChunkCollisionSettingsComponent(Objects.requireNonNull(settings, "settings")); + putOrRemoveDefault(store, + ref, + ChunkCollisionSettingsComponent.getComponentType(), + component, + component.isDefault()); + store.getResource(PhysicsRuntimeResource.getResourceType()) + .markSpaceSettingsPending(ref); + } + + public static void putSolverSettings(@Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull PhysicsSolverSettings settings) { + UUID spaceUuid = requireSpaceUuid(store, spaceId); + Ref ref = requireSpaceRef(store, spaceUuid); + putSolverSettings(store, ref, settings); + } + + public static void putSolverSettings(@Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PhysicsSolverSettings settings) { + requireSpaceUuid(store, ref); + PhysicsThreading.requireWorldThread(store, "update PhysicsStore solver settings"); + SolverSettingsComponent component = + new SolverSettingsComponent(Objects.requireNonNull(settings, "settings")); + putOrRemoveDefault(store, + ref, + SolverSettingsComponent.getComponentType(), + component, + component.isDefault()); + store.getResource(PhysicsRuntimeResource.getResourceType()) + .markSpaceSettingsPending(ref); + } + + public static void putVisualSyncSettings(@Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull PhysicsVisualSyncSettings settings) { + UUID spaceUuid = requireSpaceUuid(store, spaceId); + Ref ref = requireSpaceRef(store, spaceUuid); + putVisualSyncSettings(store, ref, settings); + } + + public static void putVisualSyncSettings(@Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PhysicsVisualSyncSettings settings) { + requireSpaceUuid(store, ref); + PhysicsThreading.requireWorldThread(store, "update PhysicsStore visual sync settings"); + VisualSyncSettingsComponent component = + new VisualSyncSettingsComponent(Objects.requireNonNull(settings, "settings")); + putOrRemoveDefault(store, + ref, + VisualSyncSettingsComponent.getComponentType(), + component, + component.isDefault()); + store.getResource(PhysicsRuntimeResource.getResourceType()) + .markSpaceSettingsPending(ref); + } + + public static void putVisualMaterializationSettings(@Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull PhysicsVisualMaterializationSettings settings) { + UUID spaceUuid = requireSpaceUuid(store, spaceId); + Ref ref = requireSpaceRef(store, spaceUuid); + putVisualMaterializationSettings(store, ref, settings); + } + + public static void putVisualMaterializationSettings(@Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PhysicsVisualMaterializationSettings settings) { + requireSpaceUuid(store, ref); + PhysicsThreading.requireWorldThread(store, + "update PhysicsStore visual materialization settings"); + VisualMaterializationSettingsComponent component = + new VisualMaterializationSettingsComponent(Objects.requireNonNull(settings, + "settings")); + putOrRemoveDefault(store, + ref, + VisualMaterializationSettingsComponent.getComponentType(), + component, + component.isDefault()); + store.getResource(PhysicsRuntimeResource.getResourceType()) + .markSpaceSettingsPending(ref); + } + + public static void putCollisionLodSettings(@Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull PhysicsCollisionLodSettings settings) { + UUID spaceUuid = requireSpaceUuid(store, spaceId); + Ref ref = requireSpaceRef(store, spaceUuid); + putCollisionLodSettings(store, ref, settings); + } + + public static void putCollisionLodSettings(@Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PhysicsCollisionLodSettings settings) { + requireSpaceUuid(store, ref); + PhysicsThreading.requireWorldThread(store, "update PhysicsStore collision LOD settings"); + CollisionLodSettingsComponent component = + new CollisionLodSettingsComponent(Objects.requireNonNull(settings, "settings")); + putOrRemoveDefault(store, + ref, + CollisionLodSettingsComponent.getComponentType(), + component, + component.isDefault()); + store.getResource(PhysicsRuntimeResource.getResourceType()) + .markSpaceSettingsPending(ref); + } + + public static void putExtensionSettings(@Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull PhysicsExtensionSettings settings) { + UUID spaceUuid = requireSpaceUuid(store, spaceId); + Ref ref = requireSpaceRef(store, spaceUuid); + putExtensionSettings(store, ref, settings); + } + + public static void putExtensionSettings(@Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PhysicsExtensionSettings settings) { + requireSpaceUuid(store, ref); + PhysicsThreading.requireWorldThread(store, "update PhysicsStore extension settings"); + ExtensionSettingsComponent component = + new ExtensionSettingsComponent(Objects.requireNonNull(settings, "settings")); + putOrRemoveDefault(store, + ref, + ExtensionSettingsComponent.getComponentType(), + component, + component.isDefault()); + store.getResource(PhysicsRuntimeResource.getResourceType()) + .markSpaceSettingsPending(ref); + } + + private static > void putOrRemoveDefault( + @Nonnull Store store, + @Nonnull Ref ref, + @Nonnull ComponentType componentType, + @Nonnull T component, + boolean defaultValue) { + if (defaultValue) { + store.removeComponentIfExists(ref, componentType); + } else { + store.putComponent(ref, componentType, component); + } + } + + public static void removeEmptySpace(@Nonnull Store store, + @Nonnull SpaceId spaceId) { + UUID spaceUuid = requireSpaceUuid(store, spaceId); + removeEmptySpace(store, spaceUuid); + } + + public static void removeEmptySpace(@Nonnull Store store, + @Nonnull UUID spaceUuid) { + Objects.requireNonNull(store, "store"); + Objects.requireNonNull(spaceUuid, "spaceUuid"); + PhysicsThreading.requireBackendIdle(store, "remove a PhysicsStore space entity"); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + PhysicsSpaceCompatibilityIndexResource compatibility = store.getResource( + PhysicsSpaceCompatibilityIndexResource.getResourceType()); + Ref ref = store.getExternalData().getRefFromUUID(spaceUuid); + BackendSpaceHandle handle = ref != null && ref.isValid() + ? runtime.getSpaceHandle(ref) + : null; + if (handle != null) { + BackendId backendId = runtime.getSpaceBackendId(ref); + PhysicsBackendRuntime backendRuntime = + backendId != null ? runtime.getRuntime(backendId) : null; + if (backendRuntime == null) { + throw new IllegalStateException("PhysicsStore space backend runtime is missing: " + + spaceUuid); + } + if (backendRuntime.bodyCount(handle.value()) > 0 + || backendRuntime.jointCount(handle.value()) > 0) { + throw new IllegalStateException("PhysicsStore space is not empty: " + spaceUuid); + } + backendRuntime.destroySpace(handle.value()); + runtime.removeSpaceHandle(ref); + } + compatibility.removeBySpaceUuid(spaceUuid); + if (ref != null && ref.isValid()) { + store.getExternalData().removeRefForUUID(spaceUuid, ref); + store.removeEntity(ref, store.getRegistry().newHolder(), RemoveReason.REMOVE); + } + } + + @Nonnull + public static UUID requireSpaceUuid(@Nonnull Store store, + @Nonnull SpaceId spaceId) { + PhysicsThreading.requireWorldThread(store, "resolve a PhysicsStore space UUID"); + UUID spaceUuid = store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .getSpaceUuid(Objects.requireNonNull(spaceId, "spaceId")); + if (spaceUuid == null) { + throw new IllegalArgumentException("PhysicsStore space id=" + spaceId.value() + + " is not registered"); + } + return spaceUuid; + } + + @Nonnull + private static Ref requireSpaceRef(@Nonnull Store store, + @Nonnull UUID spaceUuid) { + PhysicsThreading.requireWorldThread(store, "resolve a PhysicsStore space entity"); + Ref ref = store.getExternalData() + .getRefFromUUID(Objects.requireNonNull(spaceUuid, "spaceUuid")); + if (ref == null || !ref.isValid()) { + throw new IllegalArgumentException("PhysicsStore space uuid=" + spaceUuid + + " row is not registered"); + } + return ref; + } + + @Nonnull + private static UUID requireSpaceUuid(@Nonnull Store store, + @Nonnull Ref ref) { + Objects.requireNonNull(ref, "ref"); + PhysicsThreading.requireWorldThread(store, "resolve a PhysicsStore space UUID"); + if (ref.getStore() != store || !ref.isValid()) { + throw new IllegalArgumentException("PhysicsStore space entity is not valid: " + ref); + } + if (store.getComponent(ref, SpaceComponent.getComponentType()) == null) { + throw new IllegalArgumentException("PhysicsStore entity is not a space entity: " + ref); + } + UuidComponent uuid = store.getComponent(ref, UuidComponent.getComponentType()); + if (uuid == null) { + throw new IllegalArgumentException("PhysicsStore space entity has no durable UUID: " + ref); + } + return uuid.getUuid(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsStoreCleanupHooks.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsStoreCleanupHooks.java new file mode 100644 index 00000000..de13c56c --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsStoreCleanupHooks.java @@ -0,0 +1,138 @@ +package dev.hytalemodding.impulse.core.internal.physics; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Module-owned PhysicsStore cleanup callbacks. + */ +public final class PhysicsStoreCleanupHooks { + + @Nonnull + private static final Set>> FULL_STORE_CLEANUPS = + new CopyOnWriteArraySet<>(); + @Nonnull + private static final Set>> BODY_RUNTIME_CLEANUPS = + new CopyOnWriteArraySet<>(); + @Nonnull + private static final Set SPACE_CLEANUPS = new CopyOnWriteArraySet<>(); + @Nonnull + private static final Set BODY_ROW_CLEANUPS = new CopyOnWriteArraySet<>(); + + private PhysicsStoreCleanupHooks() { + } + + public static void registerFullStoreCleanup( + @Nonnull Consumer> cleanup) { + FULL_STORE_CLEANUPS.add(Objects.requireNonNull(cleanup, "cleanup")); + } + + public static void unregisterFullStoreCleanup( + @Nonnull Consumer> cleanup) { + FULL_STORE_CLEANUPS.remove(Objects.requireNonNull(cleanup, "cleanup")); + } + + public static void registerBodyRuntimeCleanup( + @Nonnull Consumer> cleanup) { + BODY_RUNTIME_CLEANUPS.add(Objects.requireNonNull(cleanup, "cleanup")); + } + + public static void unregisterBodyRuntimeCleanup( + @Nonnull Consumer> cleanup) { + BODY_RUNTIME_CLEANUPS.remove(Objects.requireNonNull(cleanup, "cleanup")); + } + + public static void registerSpaceCleanup(@Nonnull SpaceCleanup cleanup) { + SPACE_CLEANUPS.add(Objects.requireNonNull(cleanup, "cleanup")); + } + + public static void unregisterSpaceCleanup(@Nonnull SpaceCleanup cleanup) { + SPACE_CLEANUPS.remove(Objects.requireNonNull(cleanup, "cleanup")); + } + + public static void registerBodyRowCleanup(@Nonnull BodyRowCleanup cleanup) { + BODY_ROW_CLEANUPS.add(Objects.requireNonNull(cleanup, "cleanup")); + } + + public static void unregisterBodyRowCleanup(@Nonnull BodyRowCleanup cleanup) { + BODY_ROW_CLEANUPS.remove(Objects.requireNonNull(cleanup, "cleanup")); + } + + public static void clearFullStoreRuntimeResources(@Nonnull Store store) { + RuntimeException failure = null; + for (Consumer> cleanup : FULL_STORE_CLEANUPS) { + failure = run(failure, () -> cleanup.accept(store)); + } + throwIfFailed(failure); + } + + static void clearBodyRuntimeResources(@Nonnull Store store) { + RuntimeException failure = null; + for (Consumer> cleanup : BODY_RUNTIME_CLEANUPS) { + failure = run(failure, () -> cleanup.accept(store)); + } + throwIfFailed(failure); + } + + static void cleanupSpaceRuntimeResources(@Nonnull Store store, + @Nonnull UUID spaceUuid) { + RuntimeException failure = null; + for (SpaceCleanup cleanup : SPACE_CLEANUPS) { + failure = run(failure, () -> cleanup.cleanup(store, spaceUuid)); + } + throwIfFailed(failure); + } + + static void cleanupBodyRowRuntimeResources(@Nonnull Store store, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef) { + RuntimeException failure = null; + for (BodyRowCleanup cleanup : BODY_ROW_CLEANUPS) { + failure = run(failure, () -> cleanup.cleanup(store, bodyUuid, bodyRef)); + } + throwIfFailed(failure); + } + + @Nullable + private static RuntimeException run(@Nullable RuntimeException failure, + @Nonnull Runnable action) { + try { + action.run(); + return failure; + } catch (RuntimeException exception) { + if (failure == null) { + return exception; + } + failure.addSuppressed(exception); + return failure; + } + } + + private static void throwIfFailed(@Nullable RuntimeException failure) { + if (failure != null) { + throw failure; + } + } + + @FunctionalInterface + public interface SpaceCleanup { + + void cleanup(@Nonnull Store store, @Nonnull UUID spaceUuid); + } + + @FunctionalInterface + public interface BodyRowCleanup { + + void cleanup(@Nonnull Store store, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsStoreRowCleanup.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsStoreRowCleanup.java new file mode 100644 index 00000000..8a4c545c --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsStoreRowCleanup.java @@ -0,0 +1,246 @@ +package dev.hytalemodding.impulse.core.internal.physics; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.RemoveReason; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendJointHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource.BodySnapshotMetadata; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Shared cleanup for PhysicsStore-owned body and joint rows. + */ +public final class PhysicsStoreRowCleanup { + + private PhysicsStoreRowCleanup() { + } + + public static boolean removeRuntimeJoint(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull UUID jointUuid, + @Nonnull Ref jointRef) { + Ref resolvedJointRef = cleanupRefForUuid(store, jointUuid, jointRef); + BackendJointHandle jointHandle = runtime.getJointHandle(resolvedJointRef); + BackendSpaceHandle spaceHandle = runtime.getJointSpaceHandle(resolvedJointRef); + if (jointHandle == null) { + if (refMatchesUuid(resolvedJointRef, jointUuid)) { + runtime.removeJointHandle(resolvedJointRef); + } + return false; + } + BackendId backendId = runtime.getJointBackendId(resolvedJointRef); + if (!jointBindingMatchesUuid(runtime, jointUuid, backendId, spaceHandle, jointHandle)) { + return false; + } + PhysicsBackendRuntime backendRuntime = runtime.runtimeForJointRef(resolvedJointRef); + if (backendRuntime != null) { + backendRuntime.removeJoint(spaceHandle.value(), jointHandle.value()); + } + runtime.removeJointHandle(resolvedJointRef); + return true; + } + + public static boolean removeRuntimeBody(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef) { + return removeRuntimeBody(store, runtime, bodyUuid, bodyRef, null); + } + + public static boolean removeRuntimeBody(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef, + @Nullable PhysicsBackendRuntime fallbackRuntime) { + Ref resolvedBodyRef = cleanupRefForUuid(store, bodyUuid, bodyRef); + BackendBodyHandle bodyHandle = runtime.getBodyHandle(resolvedBodyRef); + BackendSpaceHandle spaceHandle = runtime.getBodySpaceHandle(resolvedBodyRef); + if (bodyHandle == null) { + if (refMatchesUuid(resolvedBodyRef, bodyUuid)) { + runtime.removeBodyHandle(resolvedBodyRef); + return false; + } + if (resolvedBodyRef.getStore() == store && resolvedBodyRef.isValid()) { + return false; + } + return runtime.removeBackendBody(bodyUuid, fallbackRuntime); + } + BackendId backendId = runtime.getBodyBackendId(resolvedBodyRef); + if (!bodyBindingMatchesUuid(runtime, bodyUuid, backendId, spaceHandle, bodyHandle)) { + return false; + } + PhysicsBackendRuntime backendRuntime = runtime.runtimeForBodyRef(resolvedBodyRef); + if (backendRuntime == null) { + backendRuntime = fallbackRuntime; + } + if (spaceHandle != null && backendRuntime != null) { + backendRuntime.removeBody(spaceHandle.value(), bodyHandle.value()); + } + runtime.removeBodyHandle(resolvedBodyRef); + return true; + } + + @Nonnull + private static Ref cleanupRefForUuid(@Nonnull Store store, + @Nonnull UUID rowUuid, + @Nonnull Ref suppliedRef) { + Ref indexedRef = store.getExternalData().getRefFromUUID(rowUuid); + if (refMatchesUuid(indexedRef, rowUuid)) { + return indexedRef; + } + if (refMatchesUuid(suppliedRef, rowUuid)) { + return suppliedRef; + } + return suppliedRef; + } + + private static boolean bodyBindingMatchesUuid(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull UUID bodyUuid, + @Nullable BackendId backendId, + @Nullable BackendSpaceHandle spaceHandle, + @Nonnull BackendBodyHandle bodyHandle) { + if (backendId == null || spaceHandle == null) { + return false; + } + BodySnapshotMetadata metadata = runtime.getBodySnapshotMetadata(backendId, + spaceHandle, + bodyHandle.value()); + return metadata != null && bodyUuid.equals(metadata.bodyUuid()); + } + + private static boolean jointBindingMatchesUuid(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull UUID jointUuid, + @Nullable BackendId backendId, + @Nullable BackendSpaceHandle spaceHandle, + @Nonnull BackendJointHandle jointHandle) { + if (backendId == null || spaceHandle == null) { + return false; + } + UUID boundJointUuid = runtime.getJointUuid(backendId, spaceHandle, jointHandle.value()); + return jointUuid.equals(boundJointUuid); + } + + private static boolean refMatchesUuid(@Nullable Ref ref, + @Nonnull UUID rowUuid) { + if (ref == null || !ref.isValid() || ref.getStore() == null) { + return false; + } + UuidComponent uuid = ref.getStore().getComponent(ref, UuidComponent.getComponentType()); + return uuid != null && rowUuid.equals(uuid.getUuid()); + } + + public static void clearBodyCopiedState(@Nonnull Store store, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef) { + clearBodyCopiedState(store, List.of(new BodyEntityRemoval(bodyUuid, bodyRef))); + } + + public static void clearBodyCopiedState(@Nonnull Store store, + @Nonnull Collection removals) { + Objects.requireNonNull(removals, "removals"); + if (removals.isEmpty()) { + return; + } + List bodyUuids = new ArrayList<>(removals.size()); + for (BodyEntityRemoval removal : removals) { + Objects.requireNonNull(removal, "removal"); + bodyUuids.add(removal.bodyUuid()); + } + store.getResource(PhysicsSnapshotResource.getResourceType()).removeBodies(bodyUuids); + } + + public static void removeBodyEntity(@Nonnull Store store, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef) { + clearBodyCopiedState(store, bodyUuid, bodyRef); + removeBodyEntityRow(store, bodyUuid, bodyRef); + } + + public static void removeBodyEntities(@Nonnull Store store, + @Nonnull Collection removals) { + Objects.requireNonNull(removals, "removals"); + if (removals.isEmpty()) { + return; + } + clearBodyCopiedState(store, removals); + for (BodyEntityRemoval removal : removals) { + removeBodyEntityRow(store, + removal.bodyUuid(), + removal.bodyRef()); + } + } + + static void removeBodyEntityRow(@Nonnull Store store, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef) { + PhysicsStoreCleanupHooks.cleanupBodyRowRuntimeResources(store, bodyUuid, bodyRef); + store.getExternalData().removeRefForUUID(bodyUuid, bodyRef); + removeEntityIfValid(store, bodyRef); + } + + public static void removeJointEntity(@Nonnull Store store, + @Nonnull UUID jointUuid, + @Nonnull Ref jointRef) { + store.getExternalData().removeRefForUUID(jointUuid, jointRef); + removeEntityIfValid(store, jointRef); + } + + public static void refreshIdentityAndRuntimeRefs(@Nonnull Store store) { + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + ConcurrentLinkedQueue uuidRefs = new ConcurrentLinkedQueue<>(); + store.forEachEntityParallel(UuidComponent.getComponentType(), (index, chunk, _) -> { + UuidComponent uuid = chunk.getComponent(index, UuidComponent.getComponentType()); + if (uuid != null) { + uuidRefs.add(new UuidRef(uuid.getUuid(), chunk.getReferenceTo(index))); + } + }); + // The UUID map is mutable store state; rebuild it on one thread. + store.getExternalData().clearUuidIndex(); + for (UuidRef uuidRef : uuidRefs) { + store.getExternalData().putRefForUUID(uuidRef.uuid(), uuidRef.ref()); + } + runtime.refreshRowRefs(store.getExternalData()); + } + + private static void removeEntityIfValid(@Nonnull Store store, + @Nonnull Ref ref) { + if (!ref.isValid()) { + return; + } + store.removeEntity(ref, store.getRegistry().newHolder(), RemoveReason.REMOVE); + } + + public record BodyEntityRemoval( + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef) { + + public BodyEntityRemoval { + Objects.requireNonNull(bodyUuid, "bodyUuid"); + Objects.requireNonNull(bodyRef, "bodyRef"); + } + } + + private record UuidRef(@Nonnull UUID uuid, + @Nonnull Ref ref) { + + private UuidRef { + Objects.requireNonNull(uuid, "uuid"); + Objects.requireNonNull(ref, "ref"); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsStoreRuntimeCleaner.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsStoreRuntimeCleaner.java new file mode 100644 index 00000000..7c1907ed --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsStoreRuntimeCleaner.java @@ -0,0 +1,42 @@ +package dev.hytalemodding.impulse.core.internal.physics; + +import com.hypixel.hytale.component.RemoveReason; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsEventResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsProfilingResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStoreReadQueueResource; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import javax.annotation.Nonnull; + +/** + * Internal full reset helper for authoritative PhysicsStore runtime state. + */ +public final class PhysicsStoreRuntimeCleaner { + + private PhysicsStoreRuntimeCleaner() { + } + + public static void clearAll(@Nonnull Store store) { + PhysicsThreading.requireBackendIdle(store, "clear PhysicsStore runtime rows"); + store.forEachEntityParallel(UuidComponent.getComponentType(), + (index, chunk, commandBuffer) -> commandBuffer.removeEntity( + chunk.getReferenceTo(index), + RemoveReason.REMOVE)); + store.getResource(PhysicsRuntimeResource.getResourceType()).destroyBackendBindings(); + store.getExternalData().clearUuidIndex(); + store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()).clear(); + store.getResource(PhysicsSnapshotResource.getResourceType()).clear(); + store.getResource(PhysicsEventResource.getResourceType()).clear(); + store.getResource(PhysicsProfilingResource.getResourceType()).reset(); + store.getResource(PhysicsStoreReadQueueResource.getResourceType()).clear(); + PhysicsStoreCleanupHooks.clearFullStoreRuntimeResources(store); + store.getResource(PhysicsRestoreStatusResource.getResourceType()) + .markRecoveredFromCleanup(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsTopologyMutations.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsTopologyMutations.java new file mode 100644 index 00000000..468b0ecf --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsTopologyMutations.java @@ -0,0 +1,295 @@ +package dev.hytalemodding.impulse.core.internal.physics; + +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsEventResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResetResult; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsStoreRowCleanup.BodyEntityRemoval; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * World-thread topology mutations for public compatibility cleanup paths. + */ +public final class PhysicsTopologyMutations { + + private PhysicsTopologyMutations() { + } + + public static void destroyBody(@Nonnull Store store, + @Nonnull UUID bodyUuid) { + UUID checkedBodyUuid = Objects.requireNonNull(bodyUuid, "bodyUuid"); + PhysicsThreading.requireBackendIdle(store, "destroy a PhysicsStore body entity"); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + Ref bodyRef = store.getExternalData().getRefFromUUID(checkedBodyUuid); + List removals = collectRows(store, null, null, checkedBodyUuid, bodyRef); + removeRuntimeRows(store, runtime, removals); + removeRows(store, removals); + } + + @Nonnull + public static PhysicsRuntimeResetResult clearBodiesKeepingSpaces( + @Nonnull Store store) { + PhysicsThreading.requireBackendIdle(store, "clear PhysicsStore body entities"); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + TopologyCounts removed = countBackendTopology(runtime); + List removals = collectRows(store, null, null, null, null); + runtime.destroyBackendBindings(); + removeRows(store, removals, false); + clearCopiedBodyState(store); + PhysicsStoreCleanupHooks.clearBodyRuntimeResources(store); + int keptSpaces = store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .size(); + store.getResource(PhysicsRestoreStatusResource.getResourceType()) + .markRecoveredFromCleanup(); + return new PhysicsRuntimeResetResult(removed.bodyCount(), + removed.jointCount(), + keptSpaces); + } + + public static void removeSpaceWithContents(@Nonnull Store store, + @Nonnull UUID spaceUuid) { + PhysicsThreading.requireBackendIdle(store, "remove a PhysicsStore space entity"); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + Ref spaceRef = store.getExternalData().getRefFromUUID(spaceUuid); + List removals = collectRows(store, spaceUuid, spaceRef, null, null); + removeRuntimeRows(store, runtime, removals); + PhysicsStoreCleanupHooks.cleanupSpaceRuntimeResources(store, spaceUuid); + removeRows(store, removals); + PhysicsSpaceMutations.removeEmptySpace(store, spaceUuid); + } + + private static void removeRuntimeRows(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull List removals) { + for (RowRemoval removal : removals) { + if (removal.kind() == RowKind.JOINT) { + removeRuntimeJoint(store, runtime, removal); + } + } + for (RowRemoval removal : removals) { + if (removal.kind() == RowKind.BODY) { + removeRuntimeBody(store, runtime, removal); + } + } + } + + private static boolean removeRuntimeJoint(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull RowRemoval removal) { + return PhysicsStoreRowCleanup.removeRuntimeJoint(store, + runtime, + removal.rowUuid(), + removal.ref()); + } + + private static boolean removeRuntimeBody(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull RowRemoval removal) { + return PhysicsStoreRowCleanup.removeRuntimeBody(store, + runtime, + removal.rowUuid(), + removal.ref()); + } + + @Nonnull + private static TopologyCounts countBackendTopology(@Nonnull PhysicsRuntimeResource runtime) { + TopologyCounts counts = new TopologyCounts(); + runtime.forEachRuntimeSpaceBinding((_, _, spaceHandle, backendRuntime) -> { + counts.addBodies(backendRuntime.bodyCount(spaceHandle.value())); + counts.addJoints(backendRuntime.jointCount(spaceHandle.value())); + }); + return counts; + } + + @Nonnull + private static List collectRows(@Nonnull Store store, + @Nullable UUID spaceUuid, + @Nullable Ref spaceRef, + @Nullable UUID bodyUuid, + @Nullable Ref bodyRef) { + ComponentType uuidType = UuidComponent.getComponentType(); + ConcurrentLinkedQueue removals = new ConcurrentLinkedQueue<>(); + store.forEachEntityParallel(uuidType, (index, chunk, _) -> { + UuidComponent uuid = chunk.getComponent(index, uuidType); + if (uuid == null) { + return; + } + UUID rowUuid = uuid.getUuid(); + Ref ref = chunk.getReferenceTo(index); + JointComponent joint = chunk.getComponent(index, JointComponent.getComponentType()); + if (matchesJoint(joint, spaceUuid, spaceRef, bodyUuid, bodyRef)) { + removals.add(new RowRemoval(ref, rowUuid, RowKind.JOINT)); + return; + } + BodyComponent body = chunk.getComponent(index, BodyComponent.getComponentType()); + if (matchesBody(body, rowUuid, ref, spaceUuid, spaceRef, bodyUuid, bodyRef)) { + removals.add(new RowRemoval(ref, rowUuid, RowKind.BODY)); + } + }); + return new ArrayList<>(removals); + } + + private static boolean matchesJoint(@Nullable JointComponent joint, + @Nullable UUID spaceUuid, + @Nullable Ref spaceRef, + @Nullable UUID bodyUuid, + @Nullable Ref bodyRef) { + if (joint == null) { + return false; + } + if (!matchesSpace(joint.getSpaceRef(), joint.getSpaceUuid(), spaceRef, spaceUuid)) { + return false; + } + return bodyUuid == null + || matchesEndpoint(joint.getBodyARef(), joint.getBodyAUuid(), bodyRef, bodyUuid) + || matchesEndpoint(joint.getBodyBRef(), joint.getBodyBUuid(), bodyRef, bodyUuid); + } + + private static boolean matchesBody(@Nullable BodyComponent body, + @Nonnull UUID rowUuid, + @Nonnull Ref rowRef, + @Nullable UUID spaceUuid, + @Nullable Ref spaceRef, + @Nullable UUID bodyUuid, + @Nullable Ref bodyRef) { + if (body == null) { + return false; + } + if (!matchesSpace(body.getSpaceRef(), body.getSpaceUuid(), spaceRef, spaceUuid)) { + return false; + } + if (bodyRef != null) { + return sameRef(rowRef, bodyRef); + } + return bodyUuid == null || bodyUuid.equals(rowUuid); + } + + private static boolean matchesSpace(@Nullable Ref rowSpaceRef, + @Nonnull UUID rowSpaceUuid, + @Nullable Ref spaceRef, + @Nullable UUID spaceUuid) { + if (spaceRef != null && rowSpaceRef != null) { + return sameRef(rowSpaceRef, spaceRef); + } + return spaceUuid == null || spaceUuid.equals(rowSpaceUuid); + } + + private static boolean matchesEndpoint(@Nullable Ref endpointRef, + @Nonnull UUID endpointUuid, + @Nullable Ref bodyRef, + @Nonnull UUID bodyUuid) { + if (bodyRef != null && endpointRef != null) { + return sameRef(endpointRef, bodyRef); + } + return bodyUuid.equals(endpointUuid); + } + + private static boolean sameRef(@Nonnull Ref first, + @Nonnull Ref second) { + return first.getStore() == second.getStore() + && first.getIndex() == second.getIndex(); + } + + private static void removeRows(@Nonnull Store store, + @Nonnull List removals) { + removeRows(store, removals, true); + } + + private static void removeRows(@Nonnull Store store, + @Nonnull List removals, + boolean clearCopiedState) { + boolean removedAny = false; + List orderedRemovals = removals.stream() + .sorted((first, second) -> Integer.compare(second.ref().getIndex(), + first.ref().getIndex())) + .toList(); + if (clearCopiedState) { + PhysicsStoreRowCleanup.clearBodyCopiedState(store, + bodyEntityRemovals(orderedRemovals)); + } + for (RowRemoval removal : orderedRemovals) { + if (!removal.ref().isValid()) { + continue; + } + if (removal.kind() == RowKind.BODY) { + PhysicsStoreRowCleanup.removeBodyEntityRow(store, + removal.rowUuid(), + removal.ref()); + } else { + PhysicsStoreRowCleanup.removeJointEntity(store, + removal.rowUuid(), + removal.ref()); + } + removedAny = true; + } + if (removedAny) { + PhysicsStoreRowCleanup.refreshIdentityAndRuntimeRefs(store); + } + } + + @Nonnull + private static List bodyEntityRemovals( + @Nonnull List removals) { + List bodyRemovals = new ArrayList<>(); + for (RowRemoval removal : removals) { + if (removal.kind() == RowKind.BODY && removal.ref().isValid()) { + bodyRemovals.add(new BodyEntityRemoval(removal.rowUuid(), + removal.ref())); + } + } + return bodyRemovals; + } + + private static void clearCopiedBodyState(@Nonnull Store store) { + store.getResource(PhysicsSnapshotResource.getResourceType()).clear(); + store.getResource(PhysicsEventResource.getResourceType()).clear(); + } + + private enum RowKind { + BODY, + JOINT + } + + private record RowRemoval(@Nonnull Ref ref, + @Nonnull UUID rowUuid, + @Nonnull RowKind kind) { + } + + private static final class TopologyCounts { + + private int bodyCount; + private int jointCount; + + private void addBodies(int count) { + bodyCount += count; + } + + private void addJoints(int count) { + jointCount += count; + } + + private int bodyCount() { + return bodyCount; + } + + private int jointCount() { + return jointCount; + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/BackendBodyHandle.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/BackendBodyHandle.java index 96c24d5c..013eab5e 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/BackendBodyHandle.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/BackendBodyHandle.java @@ -3,8 +3,8 @@ /** * Backend-local physics-body handle. * - *

This is an internal runtime identity. Stable plugin code should retain - * {@code RigidBodyKey}; core unwraps this value only at the backend-runtime boundary.

+ *

This is an internal runtime identity. Plugin-facing code should retain durable body UUIDs + * or live PhysicsStore entity refs and keep backend handles inside runtime resources.

*/ public record BackendBodyHandle(long value) { } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/BackendJointHandle.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/BackendJointHandle.java index f3395571..8349f6e4 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/BackendJointHandle.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/BackendJointHandle.java @@ -3,8 +3,8 @@ /** * Backend-local physics-joint handle. * - *

This is an internal runtime identity. Stable plugin code should retain {@code JointKey}; - * core unwraps this value only at the backend-runtime boundary.

+ *

This is an internal runtime identity. Plugin-facing code should retain durable joint UUIDs + * or live PhysicsStore entity refs and keep backend handles inside runtime resources.

*/ public record BackendJointHandle(long value) { } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsDebugResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsDebugResource.java index 59e8b015..94657d51 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsDebugResource.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsDebugResource.java @@ -2,174 +2,41 @@ import com.hypixel.hytale.component.Resource; import com.hypixel.hytale.component.ResourceType; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -import java.util.Set; -import java.util.UUID; -import javax.annotation.Nonnull; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import lombok.Getter; import lombok.Setter; +import javax.annotation.Nonnull; /** - * Runtime-only debug overlay state for one world EntityStore. - * - *

This resource intentionally keeps transient debug session state out of - * {@link PhysicsWorldResource}. Physics world state is persisted and shared - * by gameplay systems, while debug subscriptions, cadence, and packet budgets - * are temporary operational concerns.

+ * Runtime-only physics debug toggles owned by PhysicsStore. */ +@Setter @Getter -public class PhysicsDebugResource implements Resource { - - public static final float MIN_REFRESH_SECONDS = 0.05f; - public static final float MAX_REFRESH_SECONDS = 2.0f; - public static final float DEFAULT_OVERLAY_REFRESH_SECONDS = 0.10f; - public static final float DEFAULT_WORLD_COLLISION_REFRESH_SECONDS = 0.25f; +public final class PhysicsDebugResource implements Resource { - public static final double DEFAULT_VIEW_RADIUS = 96.0; - public static final int DEFAULT_MAX_BODIES = 512; - public static final int DEFAULT_MAX_CONTACTS = 384; - public static final int DEFAULT_MAX_JOINTS = 384; - public static final int DEFAULT_MAX_WORLD_COLLISION_SECTIONS = 192; - public static final int DEFAULT_MAX_WORLD_COLLISION_BOXES = 768; - - private final Set subscriberUuids = new ObjectOpenHashSet<>(); - - @Setter private boolean debugShapesEnabled = true; - @Setter private boolean debugMotionEnabled = true; - @Setter - private boolean debugContactsEnabled = true; - @Setter + private boolean debugContactsEnabled; private boolean debugJointsEnabled = true; - @Setter - private boolean debugWorldCollisionEnabled; - - private float overlayRefreshSeconds = DEFAULT_OVERLAY_REFRESH_SECONDS; - private float worldCollisionRefreshSeconds = DEFAULT_WORLD_COLLISION_REFRESH_SECONDS; - private float overlayTimeUntilRefresh; - private float worldCollisionTimeUntilRefresh; - - private double viewRadius = DEFAULT_VIEW_RADIUS; - private int maxBodies = DEFAULT_MAX_BODIES; - private int maxContacts = DEFAULT_MAX_CONTACTS; - private int maxJoints = DEFAULT_MAX_JOINTS; - private int maxWorldCollisionSections = DEFAULT_MAX_WORLD_COLLISION_SECTIONS; - private int maxWorldCollisionBoxes = DEFAULT_MAX_WORLD_COLLISION_BOXES; + private boolean debugPhysicsChunkCollisionEnabled; public PhysicsDebugResource() { } - public boolean addSubscriber(@Nonnull UUID uuid) { - return subscriberUuids.add(uuid); - } - - public boolean removeSubscriber(@Nonnull UUID uuid) { - return subscriberUuids.remove(uuid); - } - - public void clearSubscribers() { - subscriberUuids.clear(); - } - - @Nonnull - public Set getSubscriberUuids() { - return Set.copyOf(subscriberUuids); - } - - public boolean hasSubscribers() { - return !subscriberUuids.isEmpty(); - } - - public void setOverlayRefreshSeconds(float overlayRefreshSeconds) { - this.overlayRefreshSeconds = clampRefresh(overlayRefreshSeconds); - } - - public void setWorldCollisionRefreshSeconds(float worldCollisionRefreshSeconds) { - this.worldCollisionRefreshSeconds = clampRefresh(worldCollisionRefreshSeconds); - } - - public void setViewRadius(double viewRadius) { - this.viewRadius = Math.max(1.0, viewRadius); - } - - public void setMaxBodies(int maxBodies) { - this.maxBodies = Math.max(1, maxBodies); - } - - public void setMaxContacts(int maxContacts) { - this.maxContacts = Math.max(1, maxContacts); - } - - public void setMaxJoints(int maxJoints) { - this.maxJoints = Math.max(1, maxJoints); - } - - public void setMaxWorldCollisionSections(int maxWorldCollisionSections) { - this.maxWorldCollisionSections = Math.max(1, maxWorldCollisionSections); - } - - public void setMaxWorldCollisionBoxes(int maxWorldCollisionBoxes) { - this.maxWorldCollisionBoxes = Math.max(1, maxWorldCollisionBoxes); - } - - public boolean tickOverlayBudget(float dt) { - overlayTimeUntilRefresh -= dt; - if (overlayTimeUntilRefresh > 0.0f) { - return false; - } - - overlayTimeUntilRefresh += overlayRefreshSeconds; - if (overlayTimeUntilRefresh <= 0.0f) { - overlayTimeUntilRefresh = overlayRefreshSeconds; - } - return true; - } - - public boolean tickWorldCollisionBudget(float dt) { - worldCollisionTimeUntilRefresh -= dt; - if (worldCollisionTimeUntilRefresh > 0.0f) { - return false; - } - - worldCollisionTimeUntilRefresh += worldCollisionRefreshSeconds; - if (worldCollisionTimeUntilRefresh <= 0.0f) { - worldCollisionTimeUntilRefresh = worldCollisionRefreshSeconds; - } - return true; - } - @Nonnull @Override public PhysicsDebugResource clone() { PhysicsDebugResource copy = new PhysicsDebugResource(); - copy.subscriberUuids.addAll(subscriberUuids); copy.debugShapesEnabled = debugShapesEnabled; copy.debugMotionEnabled = debugMotionEnabled; copy.debugContactsEnabled = debugContactsEnabled; copy.debugJointsEnabled = debugJointsEnabled; - copy.debugWorldCollisionEnabled = debugWorldCollisionEnabled; - copy.overlayRefreshSeconds = overlayRefreshSeconds; - copy.worldCollisionRefreshSeconds = worldCollisionRefreshSeconds; - copy.overlayTimeUntilRefresh = overlayTimeUntilRefresh; - copy.worldCollisionTimeUntilRefresh = worldCollisionTimeUntilRefresh; - copy.viewRadius = viewRadius; - copy.maxBodies = maxBodies; - copy.maxContacts = maxContacts; - copy.maxJoints = maxJoints; - copy.maxWorldCollisionSections = maxWorldCollisionSections; - copy.maxWorldCollisionBoxes = maxWorldCollisionBoxes; + copy.debugPhysicsChunkCollisionEnabled = debugPhysicsChunkCollisionEnabled; return copy; } - public static ResourceType getResourceType() { - return ImpulsePlugin.get().getPhysicsDebugResourceType(); - } - - private static float clampRefresh(float value) { - return Math.clamp(value, MIN_REFRESH_SECONDS, MAX_REFRESH_SECONDS); + @Nonnull + public static ResourceType getResourceType() { + return PhysicsResourceTypes.debugResourceType(); } } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsEventResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsEventResource.java new file mode 100644 index 00000000..f80a42f0 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsEventResource.java @@ -0,0 +1,78 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsFrameEvent; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsStepEvent; +import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * Latest copied PhysicsStore event frame for EntityStore publication and diagnostics. + */ +public final class PhysicsEventResource implements Resource { + + @Nonnull + private volatile PhysicsEventFrame latestFrame = PhysicsEventFrame.empty(0L); + private long nextFrameSequence; + + public PhysicsEventResource() { + } + + @Nonnull + public PhysicsEventFrame getLatestFrame() { + return latestFrame; + } + + @Nonnull + public PhysicsEventFrame publishStepFrame(long snapshotSequence, + long serverTick, + int bodyCount, + long stepNanos, + long snapshotNanos, + @Nonnull List physicsEvents, + int droppedBackendEventCount) { + long safeSnapshotSequence = Math.max(0L, snapshotSequence); + PhysicsStepEvent stepEvent = new PhysicsStepEvent(safeSnapshotSequence, + serverTick, + safeSnapshotSequence, + PublishedPhysicsSnapshotFrame.Status.COMPLETE, + bodyCount, + stepNanos, + snapshotNanos); + PhysicsEventFrame frame = new PhysicsEventFrame(++nextFrameSequence, + safeSnapshotSequence, + safeSnapshotSequence, + safeSnapshotSequence, + serverTick, + List.of(stepEvent), + List.of(), + Objects.requireNonNull(physicsEvents, "physicsEvents"), + droppedBackendEventCount); + latestFrame = frame; + return frame; + } + + public void clear() { + latestFrame = PhysicsEventFrame.empty(0L); + nextFrameSequence = 0L; + } + + @Nonnull + @Override + public PhysicsEventResource clone() { + PhysicsEventResource copy = new PhysicsEventResource(); + copy.latestFrame = latestFrame; + copy.nextFrameSequence = nextFrameSequence; + return copy; + } + + @Nonnull + public static ResourceType getResourceType() { + return PhysicsResourceTypes.eventResourceType(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsProfilingResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsProfilingResource.java new file mode 100644 index 00000000..aea395af --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsProfilingResource.java @@ -0,0 +1,162 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsStepPhaseStats; +import lombok.Getter; +import lombok.Setter; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * Runtime-only PhysicsStore profiling counters split by phase. + */ +public final class PhysicsProfilingResource implements Resource { + + @Setter + @Getter + private boolean enabled; + @Getter + private long snapshotNanos; + @Getter + private long stepSubmitNanos; + @Getter + private int spaces; + @Getter + private int substeps; + @Getter + private int publishedBodies; + private int schedulerSamples; + private float schedulerInputDtSeconds; + private float schedulerSubmittedDtSeconds; + private float schedulerBacklogDtSeconds; + private float droppedBacklogDtSeconds; + private boolean dtCapHit; + @Nonnull + private PhysicsStepPhaseStats nativePhaseStats = PhysicsStepPhaseStats.unavailable(); + + public PhysicsProfilingResource() { + } + + public void recordStep(long stepSubmitNanos, + int spaces, + int substeps, + @Nonnull PhysicsStepPhaseStats nativePhaseStats) { + this.stepSubmitNanos = Math.max(0L, stepSubmitNanos); + this.spaces = Math.max(0, spaces); + this.substeps = Math.max(0, substeps); + this.nativePhaseStats = Objects.requireNonNull(nativePhaseStats, "nativePhaseStats"); + } + + public void recordSnapshot(long snapshotNanos, int publishedBodies) { + this.snapshotNanos = Math.max(0L, snapshotNanos); + this.publishedBodies = Math.max(0, publishedBodies); + } + + public void recordStepScheduling(float inputDtSeconds, + float submittedDtSeconds, + float backlogDtSeconds, + float droppedBacklogDtSeconds, + boolean dtCapHit) { + schedulerSamples = 1; + schedulerInputDtSeconds = safeDt(inputDtSeconds); + schedulerSubmittedDtSeconds = safeDt(submittedDtSeconds); + schedulerBacklogDtSeconds = safeDt(backlogDtSeconds); + this.droppedBacklogDtSeconds = safeDt(droppedBacklogDtSeconds); + this.dtCapHit = dtCapHit; + } + + public void reset() { + snapshotNanos = 0L; + stepSubmitNanos = 0L; + spaces = 0; + substeps = 0; + publishedBodies = 0; + schedulerSamples = 0; + schedulerInputDtSeconds = 0.0f; + schedulerSubmittedDtSeconds = 0.0f; + schedulerBacklogDtSeconds = 0.0f; + droppedBacklogDtSeconds = 0.0f; + dtCapHit = false; + nativePhaseStats = PhysicsStepPhaseStats.unavailable(); + } + + @Nonnull + public StepSample latestStepSample() { + return new StepSample(spaces, + substeps, + stepSubmitNanos, + snapshotNanos, + publishedBodies, + schedulerSamples, + schedulerInputDtSeconds, + schedulerSubmittedDtSeconds, + schedulerBacklogDtSeconds, + droppedBacklogDtSeconds, + dtCapHit, + nativePhaseStats); + } + + @Nonnull + public PhysicsStepPhaseStats getNativePhaseStats() { + return nativePhaseStats; + } + + @Nonnull + @Override + public PhysicsProfilingResource clone() { + PhysicsProfilingResource copy = new PhysicsProfilingResource(); + copy.enabled = enabled; + copy.snapshotNanos = snapshotNanos; + copy.stepSubmitNanos = stepSubmitNanos; + copy.spaces = spaces; + copy.substeps = substeps; + copy.publishedBodies = publishedBodies; + copy.schedulerSamples = schedulerSamples; + copy.schedulerInputDtSeconds = schedulerInputDtSeconds; + copy.schedulerSubmittedDtSeconds = schedulerSubmittedDtSeconds; + copy.schedulerBacklogDtSeconds = schedulerBacklogDtSeconds; + copy.droppedBacklogDtSeconds = droppedBacklogDtSeconds; + copy.dtCapHit = dtCapHit; + copy.nativePhaseStats = nativePhaseStats; + return copy; + } + + @Nonnull + public static ResourceType getResourceType() { + return PhysicsResourceTypes.profilingResourceType(); + } + + public record StepSample(int spaces, + int substeps, + long stepSubmitNanos, + long snapshotNanos, + int publishedBodies, + int schedulerSamples, + float schedulerInputDtSeconds, + float schedulerSubmittedDtSeconds, + float schedulerBacklogDtSeconds, + float droppedBacklogDtSeconds, + boolean dtCapHit, + @Nonnull PhysicsStepPhaseStats nativePhaseStats) { + + public StepSample { + spaces = Math.max(0, spaces); + substeps = Math.max(0, substeps); + stepSubmitNanos = Math.max(0L, stepSubmitNanos); + snapshotNanos = Math.max(0L, snapshotNanos); + publishedBodies = Math.max(0, publishedBodies); + schedulerSamples = Math.max(0, schedulerSamples); + schedulerInputDtSeconds = safeDt(schedulerInputDtSeconds); + schedulerSubmittedDtSeconds = safeDt(schedulerSubmittedDtSeconds); + schedulerBacklogDtSeconds = safeDt(schedulerBacklogDtSeconds); + droppedBacklogDtSeconds = safeDt(droppedBacklogDtSeconds); + Objects.requireNonNull(nativePhaseStats, "nativePhaseStats"); + } + } + + private static float safeDt(float dtSeconds) { + return Float.isFinite(dtSeconds) ? Math.max(0.0f, dtSeconds) : 0.0f; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsProjectionIndexResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsProjectionIndexResource.java new file mode 100644 index 00000000..a323e8da --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsProjectionIndexResource.java @@ -0,0 +1,442 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Runtime-only EntityStore projection index for authoritative PhysicsStore attachments. + */ +public final class PhysicsProjectionIndexResource implements Resource { + + @Nullable + private static ResourceType resourceType; + + private final Map>> bodyAttachments = + new Object2ObjectOpenHashMap<>(); + private final Int2ObjectOpenHashMap bodyAttachmentsByRowIndex = + new Int2ObjectOpenHashMap<>(); + private final Map> generatedVisualProxies = + new Object2ObjectOpenHashMap<>(); + private final Int2ObjectOpenHashMap generatedVisualProxiesByRowIndex = + new Int2ObjectOpenHashMap<>(); + + public synchronized void registerAttachment(@Nonnull UUID bodyUuid, + @Nonnull Ref attachment) { + registerAttachment(bodyUuid, null, attachment); + } + + public synchronized void registerAttachment(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef, + @Nonnull Ref attachment) { + bodyAttachments.computeIfAbsent(bodyUuid, _ -> new ObjectOpenHashSet<>()) + .add(attachment); + if (bodyRef != null) { + bodyAttachmentRefs(bodyRef) + .add(attachment); + } + } + + public synchronized void unregisterAttachment(@Nonnull UUID bodyUuid, + @Nonnull Ref attachment) { + unregisterAttachment(bodyUuid, null, attachment); + } + + public synchronized void unregisterAttachment(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef, + @Nonnull Ref attachment) { + Set> attachments = bodyAttachments.get(bodyUuid); + if (attachments != null) { + attachments.remove(attachment); + if (attachments.isEmpty()) { + bodyAttachments.remove(bodyUuid); + } + } + if (bodyRef != null) { + unregisterAttachmentRef(bodyRef, attachment); + } + } + + @Nonnull + public Collection> getAttachments(@Nonnull UUID bodyUuid) { + return liveAttachments(bodyAttachments, bodyUuid); + } + + @Nonnull + public Collection> getAttachments(@Nonnull Ref bodyRef) { + return liveAttachments(bodyRef); + } + + @Nonnull + private Collection> liveAttachments( + @Nonnull Map>> attachmentsByKey, + @Nonnull K key) { + List> liveAttachments = new ArrayList<>(); + synchronized (this) { + Set> attachments = attachmentsByKey.get(key); + if (attachments == null || attachments.isEmpty()) { + return List.of(); + } + for (Iterator> iterator = attachments.iterator(); iterator.hasNext();) { + Ref attachment = iterator.next(); + if (attachment != null && attachment.isValid()) { + liveAttachments.add(attachment); + } else { + iterator.remove(); + } + } + if (attachments.isEmpty()) { + attachmentsByKey.remove(key); + } + } + return liveAttachments; + } + + public boolean hasAttachments(@Nonnull UUID bodyUuid) { + return hasLiveAttachments(bodyAttachments, bodyUuid); + } + + public boolean hasAttachments(@Nonnull Ref bodyRef) { + return hasLiveAttachments(bodyRef); + } + + private boolean hasLiveAttachments(@Nonnull Map>> attachmentsByKey, + @Nonnull K key) { + synchronized (this) { + Set> attachments = attachmentsByKey.get(key); + if (attachments == null || attachments.isEmpty()) { + return false; + } + boolean hasLiveAttachment = false; + for (Iterator> iterator = attachments.iterator(); iterator.hasNext();) { + Ref attachment = iterator.next(); + if (attachment != null && attachment.isValid()) { + hasLiveAttachment = true; + } else { + iterator.remove(); + } + } + if (attachments.isEmpty()) { + attachmentsByKey.remove(key); + } + return hasLiveAttachment; + } + } + + @Nullable + public Ref getGeneratedVisualProxy(@Nonnull UUID bodyUuid) { + return liveGeneratedVisualProxy(generatedVisualProxies, bodyUuid); + } + + @Nullable + public Ref getGeneratedVisualProxy(@Nonnull Ref bodyRef) { + return liveGeneratedVisualProxy(bodyRef); + } + + public int generatedVisualProxyCount() { + int count = 0; + synchronized (this) { + for (Iterator>> iterator = + generatedVisualProxies.entrySet().iterator(); iterator.hasNext();) { + Ref proxy = iterator.next().getValue(); + if (proxy != null && proxy.isValid()) { + count++; + } else { + iterator.remove(); + } + } + } + return count; + } + + @Nullable + private Ref liveGeneratedVisualProxy( + @Nonnull Map> proxiesByKey, + @Nonnull K key) { + synchronized (this) { + Ref proxy = proxiesByKey.get(key); + if (proxy != null && proxy.isValid()) { + return proxy; + } + proxiesByKey.remove(key); + return null; + } + } + + public void setGeneratedVisualProxy(@Nonnull UUID bodyUuid, + @Nonnull Ref proxy) { + setGeneratedVisualProxy(bodyUuid, null, proxy); + } + + public void setGeneratedVisualProxy(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef, + @Nonnull Ref proxy) { + synchronized (this) { + generatedVisualProxies.put(bodyUuid, proxy); + if (bodyRef != null) { + generatedVisualProxiesByRowIndex.put(bodyRef.getIndex(), + new GeneratedVisualProxyRef(bodyUuid, bodyRef, proxy)); + } + } + } + + public void clearGeneratedVisualProxy(@Nonnull UUID bodyUuid) { + clearGeneratedVisualProxyForBodyRef(bodyUuid, null); + } + + public void clearGeneratedVisualProxyForBodyRef(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef) { + synchronized (this) { + generatedVisualProxies.remove(bodyUuid); + if (bodyRef != null) { + clearGeneratedVisualProxyRef(bodyRef); + } + } + } + + public void clearGeneratedVisualProxy(@Nonnull UUID bodyUuid, + @Nonnull Ref expectedProxy) { + clearGeneratedVisualProxy(bodyUuid, null, expectedProxy); + } + + public void clearGeneratedVisualProxy(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef, + @Nonnull Ref expectedProxy) { + synchronized (this) { + Ref proxy = generatedVisualProxies.get(bodyUuid); + if (sameRef(proxy, expectedProxy)) { + generatedVisualProxies.remove(bodyUuid); + } + if (bodyRef != null) { + clearGeneratedVisualProxyRef(bodyRef, expectedProxy); + } + } + } + + public void updateAttachmentBodyRef(@Nonnull UUID bodyUuid, + @Nullable Ref oldBodyRef, + @Nullable Ref newBodyRef, + @Nonnull Ref attachment, + boolean generatedProxy) { + synchronized (this) { + if (sameRef(oldBodyRef, newBodyRef)) { + return; + } + if (oldBodyRef != null) { + unregisterAttachmentRef(oldBodyRef, attachment); + if (generatedProxy) { + clearGeneratedVisualProxyRef(oldBodyRef, attachment); + } + } + if (newBodyRef != null) { + bodyAttachmentRefs(newBodyRef) + .add(attachment); + if (generatedProxy) { + generatedVisualProxiesByRowIndex.put(newBodyRef.getIndex(), + new GeneratedVisualProxyRef(bodyUuid, newBodyRef, attachment)); + } + } + } + } + + @Nonnull + @Override + public PhysicsProjectionIndexResource clone() { + PhysicsProjectionIndexResource copy = new PhysicsProjectionIndexResource(); + synchronized (this) { + for (Map.Entry>> entry : bodyAttachments.entrySet()) { + copy.bodyAttachments.put(entry.getKey(), new ObjectOpenHashSet<>(entry.getValue())); + } + for (var entry : bodyAttachmentsByRowIndex.int2ObjectEntrySet()) { + BodyAttachmentRefs refs = entry.getValue(); + copy.bodyAttachmentsByRowIndex.put(entry.getIntKey(), + new BodyAttachmentRefs(refs.bodyRef(), + new ObjectOpenHashSet<>(refs.attachments()))); + } + copy.generatedVisualProxies.putAll(generatedVisualProxies); + for (var entry : generatedVisualProxiesByRowIndex.int2ObjectEntrySet()) { + GeneratedVisualProxyRef proxy = entry.getValue(); + copy.generatedVisualProxiesByRowIndex.put(entry.getIntKey(), + new GeneratedVisualProxyRef(proxy.bodyUuid(), proxy.bodyRef(), proxy.proxy())); + } + } + return copy; + } + + public static ResourceType getResourceType() { + return resourceType; + } + + public static void setResourceType( + @Nonnull ResourceType type) { + resourceType = type; + } + + public static void clearResourceType() { + resourceType = null; + } + + private void unregisterAttachmentRef(@Nonnull Ref bodyRef, + @Nonnull Ref attachment) { + int rowIndex = bodyRef.getIndex(); + BodyAttachmentRefs row = bodyAttachmentsByRowIndex.get(rowIndex); + if (row == null || !sameRef(row.bodyRef(), bodyRef)) { + if (row != null) { + bodyAttachmentsByRowIndex.remove(rowIndex); + } + return; + } + Set> attachments = row.attachments(); + attachments.remove(attachment); + if (attachments.isEmpty()) { + bodyAttachmentsByRowIndex.remove(rowIndex); + } + } + + private void clearGeneratedVisualProxyRef(@Nonnull Ref bodyRef, + @Nonnull Ref expectedProxy) { + int rowIndex = bodyRef.getIndex(); + GeneratedVisualProxyRef row = generatedVisualProxiesByRowIndex.get(rowIndex); + if (row != null + && (!sameRef(row.bodyRef(), bodyRef) + || sameRef(row.proxy(), expectedProxy))) { + generatedVisualProxiesByRowIndex.remove(rowIndex); + } + } + + private void clearGeneratedVisualProxyRef(@Nonnull Ref bodyRef) { + int rowIndex = bodyRef.getIndex(); + GeneratedVisualProxyRef row = generatedVisualProxiesByRowIndex.get(rowIndex); + if (row != null && sameRef(row.bodyRef(), bodyRef)) { + generatedVisualProxiesByRowIndex.remove(rowIndex); + } + } + + @Nonnull + private Set> bodyAttachmentRefs(@Nonnull Ref bodyRef) { + int rowIndex = bodyRef.getIndex(); + BodyAttachmentRefs row = bodyAttachmentsByRowIndex.get(rowIndex); + if (row == null || !sameRef(row.bodyRef(), bodyRef)) { + row = new BodyAttachmentRefs(bodyRef, new ObjectOpenHashSet<>()); + bodyAttachmentsByRowIndex.put(rowIndex, row); + } + return row.attachments(); + } + + @Nonnull + private Collection> liveAttachments(@Nonnull Ref bodyRef) { + List> liveAttachments = new ArrayList<>(); + synchronized (this) { + int rowIndex = bodyRef.getIndex(); + BodyAttachmentRefs row = bodyAttachmentsByRowIndex.get(rowIndex); + if (row == null) { + return List.of(); + } + if (!sameRef(row.bodyRef(), bodyRef)) { + bodyAttachmentsByRowIndex.remove(rowIndex); + return List.of(); + } + Set> attachments = row.attachments(); + if (attachments.isEmpty()) { + bodyAttachmentsByRowIndex.remove(rowIndex); + return List.of(); + } + for (Iterator> iterator = attachments.iterator(); iterator.hasNext();) { + Ref attachment = iterator.next(); + if (attachment != null && attachment.isValid()) { + liveAttachments.add(attachment); + } else { + iterator.remove(); + } + } + if (attachments.isEmpty()) { + bodyAttachmentsByRowIndex.remove(rowIndex); + } + } + return liveAttachments; + } + + private boolean hasLiveAttachments(@Nonnull Ref bodyRef) { + synchronized (this) { + int rowIndex = bodyRef.getIndex(); + BodyAttachmentRefs row = bodyAttachmentsByRowIndex.get(rowIndex); + if (row == null) { + return false; + } + if (!sameRef(row.bodyRef(), bodyRef)) { + bodyAttachmentsByRowIndex.remove(rowIndex); + return false; + } + Set> attachments = row.attachments(); + if (attachments.isEmpty()) { + bodyAttachmentsByRowIndex.remove(rowIndex); + return false; + } + boolean hasLiveAttachment = false; + for (Iterator> iterator = attachments.iterator(); iterator.hasNext();) { + Ref attachment = iterator.next(); + if (attachment != null && attachment.isValid()) { + hasLiveAttachment = true; + } else { + iterator.remove(); + } + } + if (attachments.isEmpty()) { + bodyAttachmentsByRowIndex.remove(rowIndex); + } + return hasLiveAttachment; + } + } + + @Nullable + private Ref liveGeneratedVisualProxy(@Nonnull Ref bodyRef) { + synchronized (this) { + int rowIndex = bodyRef.getIndex(); + GeneratedVisualProxyRef row = generatedVisualProxiesByRowIndex.get(rowIndex); + if (row == null) { + return null; + } + if (!sameRef(row.bodyRef(), bodyRef)) { + generatedVisualProxiesByRowIndex.remove(rowIndex); + return null; + } + Ref proxy = row.proxy(); + if (proxy != null && proxy.isValid()) { + return proxy; + } + generatedVisualProxiesByRowIndex.remove(rowIndex); + return null; + } + } + + private static boolean sameRef(@Nullable Ref first, + @Nullable Ref second) { + return first == second + || first != null && second != null + && first.getStore() == second.getStore() + && first.getIndex() == second.getIndex(); + } + + private record BodyAttachmentRefs(@Nonnull Ref bodyRef, + @Nonnull Set> attachments) { + } + + private record GeneratedVisualProxyRef(@Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef, + @Nonnull Ref proxy) { + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsResourceTypes.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsResourceTypes.java new file mode 100644 index 00000000..6f9526d1 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsResourceTypes.java @@ -0,0 +1,126 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Registered Hytale ECS resource type handles for the internal PhysicsStore runtime. + */ +public final class PhysicsResourceTypes { + + @Nullable + private static ResourceType runtimeResourceType; + @Nullable + private static ResourceType worldSettingsResourceType; + @Nullable + private static ResourceType stepSchedulerResourceType; + @Nullable + private static ResourceType + spaceCompatibilityIndexResourceType; + @Nullable + private static ResourceType snapshotResourceType; + @Nullable + private static ResourceType eventResourceType; + @Nullable + private static ResourceType readQueueResourceType; + @Nullable + private static ResourceType restoreStatusResourceType; + @Nullable + private static ResourceType profilingResourceType; + @Nullable + private static ResourceType debugResourceType; + + private PhysicsResourceTypes() { + } + + public static void registerResourceTypes( + @Nonnull ComponentRegistryProxy registry) { + runtimeResourceType = registry.registerResource( + PhysicsRuntimeResource.class, + PhysicsRuntimeResource::new); + worldSettingsResourceType = registry.registerResource( + PhysicsWorldSettingsResource.class, + PhysicsWorldSettingsResource::new); + stepSchedulerResourceType = registry.registerResource( + PhysicsStepSchedulerResource.class, + PhysicsStepSchedulerResource::new); + spaceCompatibilityIndexResourceType = registry.registerResource( + PhysicsSpaceCompatibilityIndexResource.class, + PhysicsSpaceCompatibilityIndexResource::new); + snapshotResourceType = registry.registerResource( + PhysicsSnapshotResource.class, + PhysicsSnapshotResource::new); + eventResourceType = registry.registerResource( + PhysicsEventResource.class, + PhysicsEventResource::new); + readQueueResourceType = registry.registerResource( + PhysicsStoreReadQueueResource.class, + PhysicsStoreReadQueueResource::new); + restoreStatusResourceType = registry.registerResource( + PhysicsRestoreStatusResource.class, + PhysicsRestoreStatusResource::new); + profilingResourceType = registry.registerResource( + PhysicsProfilingResource.class, + PhysicsProfilingResource::new); + debugResourceType = registry.registerResource( + PhysicsDebugResource.class, + PhysicsDebugResource::new); + } + + @Nonnull + public static ResourceType runtimeResourceType() { + return runtimeResourceType; + } + + @Nonnull + public static ResourceType worldSettingsResourceType() { + return worldSettingsResourceType; + } + + @Nonnull + public static ResourceType stepSchedulerResourceType() { + return stepSchedulerResourceType; + } + + @Nonnull + public static ResourceType + spaceCompatibilityIndexResourceType() { + return spaceCompatibilityIndexResourceType; + } + + @Nonnull + public static ResourceType snapshotResourceType() { + return snapshotResourceType; + } + + @Nonnull + public static ResourceType eventResourceType() { + return eventResourceType; + } + + @Nonnull + public static ResourceType readQueueResourceType() { + return readQueueResourceType; + } + + @Nonnull + public static ResourceType restoreStatusResourceType() { + return restoreStatusResourceType; + } + + @Nonnull + public static ResourceType profilingResourceType() { + return profilingResourceType; + } + + @Nonnull + public static ResourceType debugResourceType() { + return debugResourceType; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsRestoreStatusResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsRestoreStatusResource.java new file mode 100644 index 00000000..6a8d59db --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsRestoreStatusResource.java @@ -0,0 +1,93 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import lombok.Getter; +import javax.annotation.Nonnull; + +/** + * Runtime-only restore status and skip accounting. + */ +public final class PhysicsRestoreStatusResource implements Resource { + + @Getter + private boolean pending; + @Getter + private boolean failed; + @Getter + private boolean hydrated; + @Nonnull + private String failureMessage = ""; + @Nonnull + private final Object2IntMap softSkipsByReason = new Object2IntLinkedOpenHashMap<>(); + + public PhysicsRestoreStatusResource() { + } + + public void markPending() { + pending = true; + failed = false; + hydrated = false; + failureMessage = ""; + softSkipsByReason.clear(); + } + + @Nonnull + public String getFailureMessage() { + return failureMessage; + } + + public void markFailed(@Nonnull String failureMessage) { + pending = false; + failed = true; + hydrated = false; + this.failureMessage = failureMessage; + } + + public void markComplete() { + pending = false; + failed = false; + failureMessage = ""; + } + + public void markHydrated() { + hydrated = true; + } + + public void markRecoveredFromCleanup() { + pending = false; + failed = false; + hydrated = true; + failureMessage = ""; + softSkipsByReason.clear(); + } + + public void recordSoftSkip(@Nonnull String reason) { + softSkipsByReason.put(reason, softSkipsByReason.getInt(reason) + 1); + } + + @Nonnull + public Object2IntMap getSoftSkipsByReason() { + return new Object2IntLinkedOpenHashMap<>(softSkipsByReason); + } + + @Nonnull + @Override + public PhysicsRestoreStatusResource clone() { + PhysicsRestoreStatusResource copy = new PhysicsRestoreStatusResource(); + copy.pending = pending; + copy.failed = failed; + copy.hydrated = hydrated; + copy.failureMessage = failureMessage; + copy.softSkipsByReason.putAll(softSkipsByReason); + return copy; + } + + @Nonnull + public static ResourceType getResourceType() { + return PhysicsResourceTypes.restoreStatusResourceType(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsRuntimeResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsRuntimeResource.java new file mode 100644 index 00000000..55891152 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsRuntimeResource.java @@ -0,0 +1,1049 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.longs.LongArrayList; +import it.unimi.dsi.fastutil.longs.LongList; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.function.LongConsumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.Getter; +import lombok.Setter; + +/** + * Runtime-only backend bindings for PhysicsStore spaces, bodies, and joints. + */ +public final class PhysicsRuntimeResource implements Resource { + + @Nonnull + private final Map runtimesByBackend = + new Object2ObjectOpenHashMap<>(); + @Nonnull + private final Int2ObjectOpenHashMap> spaceRefsByRowIndex = + new Int2ObjectOpenHashMap<>(); + @Nonnull + private final Int2ObjectOpenHashMap spaceHandlesByRowIndex = + new Int2ObjectOpenHashMap<>(); + @Nonnull + private final Int2ObjectOpenHashMap backendIdsBySpaceRowIndex = + new Int2ObjectOpenHashMap<>(); + @Nonnull + private final Map spaceMetadataByKey = + new Object2ObjectOpenHashMap<>(); + @Nonnull + private final Int2ObjectOpenHashMap> bodyRefsByRowIndex = + new Int2ObjectOpenHashMap<>(); + @Nonnull + private final Int2ObjectOpenHashMap bodyHandlesByRowIndex = + new Int2ObjectOpenHashMap<>(); + @Nonnull + private final Int2ObjectOpenHashMap bodySpaceHandlesByRowIndex = + new Int2ObjectOpenHashMap<>(); + @Nonnull + private final Int2ObjectOpenHashMap backendIdsByBodyRowIndex = + new Int2ObjectOpenHashMap<>(); + @Nonnull + private final Int2ObjectOpenHashMap jointHandlesByRowIndex = + new Int2ObjectOpenHashMap<>(); + @Nonnull + private final Int2ObjectOpenHashMap jointSpaceHandlesByRowIndex = + new Int2ObjectOpenHashMap<>(); + @Nonnull + private final Int2ObjectOpenHashMap> jointRefsByRowIndex = + new Int2ObjectOpenHashMap<>(); + @Nonnull + private final Int2ObjectOpenHashMap backendIdsByJointRowIndex = + new Int2ObjectOpenHashMap<>(); + @Nonnull + private final Map jointMetadataByKey = + new Object2ObjectOpenHashMap<>(); + @Nonnull + private final Map jointKeysByUuid = + new Object2ObjectOpenHashMap<>(); + @Nonnull + private final Int2ObjectOpenHashMap chunkCollisionPayloadKeysByRowIndex = + new Int2ObjectOpenHashMap<>(); + @Nonnull + private final Map bodyHandlesBySpaceKey = + new Object2ObjectOpenHashMap<>(); + @Nonnull + private final Map bodyHitMetadataByKey = + new Object2ObjectOpenHashMap<>(); + @Nonnull + private final Map bodySnapshotMetadataByKey = + new Object2ObjectOpenHashMap<>(); + @Nonnull + private final Map bodySnapshotKeysByUuid = + new Object2ObjectOpenHashMap<>(); + @Nonnull + private final List pendingBodyOperations = new ArrayList<>(); + @Nonnull + private final Int2ObjectOpenHashMap> pendingSpaceSettingsByRowIndex = + new Int2ObjectOpenHashMap<>(); + private long registrationTopologyGeneration; + @Setter + @Getter + private boolean started; + + public PhysicsRuntimeResource() { + } + + public void putRuntime(@Nonnull BackendId backendId, @Nonnull PhysicsBackendRuntime runtime) { + runtimesByBackend.put(Objects.requireNonNull(backendId, "backendId"), + Objects.requireNonNull(runtime, "runtime")); + } + + @Nullable + public PhysicsBackendRuntime getRuntime(@Nonnull BackendId backendId) { + return runtimesByBackend.get(backendId); + } + + public void putSpaceHandle(@Nonnull Ref spaceRef, + @Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle handle) { + Ref checkedSpaceRef = Objects.requireNonNull(spaceRef, "spaceRef"); + BackendId checkedBackendId = Objects.requireNonNull(backendId, "backendId"); + BackendSpaceHandle checkedHandle = Objects.requireNonNull(handle, "handle"); + int rowIndex = checkedSpaceRef.getIndex(); + BackendId previousBackendId = backendIdsBySpaceRowIndex.get(rowIndex); + BackendSpaceHandle previousHandle = spaceHandlesByRowIndex.get(rowIndex); + if (previousBackendId != null && previousHandle != null) { + removeSpaceMetadata(new BackendSpaceKey(previousBackendId, previousHandle)); + } + spaceRefsByRowIndex.put(rowIndex, checkedSpaceRef); + backendIdsBySpaceRowIndex.put(rowIndex, checkedBackendId); + spaceHandlesByRowIndex.put(rowIndex, checkedHandle); + markRegistrationTopologyChanged(); + } + + public void putSpaceMetadata(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle handle, + @Nonnull UUID spaceUuid, + @Nonnull Ref spaceRef) { + BackendSpaceKey key = new BackendSpaceKey(backendId, handle); + removeSpaceMetadataForUuid(spaceUuid, key); + spaceMetadataByKey.put(key, new SpaceMetadata(spaceUuid, spaceRef)); + } + + @Nullable + public BackendSpaceHandle getSpaceHandle(@Nonnull Ref spaceRef) { + return spaceHandlesByRowIndex.get(spaceRef.getIndex()); + } + + @Nullable + public UUID getSpaceUuid(@Nonnull Ref spaceRef) { + BackendSpaceKey key = spaceKey(spaceRef); + SpaceMetadata metadata = key != null ? spaceMetadataByKey.get(key) : null; + return metadata != null ? metadata.spaceUuid() : null; + } + + @Nullable + public BackendId getSpaceBackendId(@Nonnull Ref spaceRef) { + return backendIdsBySpaceRowIndex.get(spaceRef.getIndex()); + } + + public void removeSpaceHandle(@Nonnull Ref spaceRef) { + int rowIndex = Objects.requireNonNull(spaceRef, "spaceRef").getIndex(); + BackendId backendId = backendIdsBySpaceRowIndex.remove(rowIndex); + BackendSpaceHandle removed = spaceHandlesByRowIndex.remove(rowIndex); + spaceRefsByRowIndex.remove(rowIndex); + if (backendId == null || removed == null) { + return; + } + removeSpaceMetadata(new BackendSpaceKey(backendId, removed)); + markRegistrationTopologyChanged(); + } + + private void removeSpaceMetadata(@Nonnull BackendSpaceKey key) { + spaceMetadataByKey.remove(key); + LongList bodyHandles = bodyHandlesBySpaceKey.remove(key); + if (bodyHandles != null) { + bodyHandles.forEach((long bodyHandle) -> removeBodyMetadata( + new BackendBodyKey(key.backendId(), key.spaceHandle(), bodyHandle))); + } + List removedJointKeys = new ArrayList<>(); + jointMetadataByKey.keySet().forEach(jointKey -> { + if (jointKey.backendId().equals(key.backendId()) + && jointKey.spaceHandle() == key.spaceHandle()) { + removedJointKeys.add(jointKey); + } + }); + removedJointKeys.forEach(this::removeJointMetadata); + } + + private void removeSpaceMetadataForUuid(@Nonnull UUID spaceUuid, + @Nonnull BackendSpaceKey replacementKey) { + List removedKeys = new ArrayList<>(); + spaceMetadataByKey.forEach((key, metadata) -> { + if (!key.equals(replacementKey) && metadata.spaceUuid().equals(spaceUuid)) { + removedKeys.add(key); + } + }); + removedKeys.forEach(this::removeSpaceMetadata); + } + + public void putBodyHandle(@Nonnull Ref bodyRef, + @Nonnull Ref spaceRef, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull BackendBodyHandle handle) { + Ref checkedBodyRef = Objects.requireNonNull(bodyRef, "bodyRef"); + BackendId backendId = getSpaceBackendId(Objects.requireNonNull(spaceRef, "spaceRef")); + int rowIndex = checkedBodyRef.getIndex(); + BackendId previousBackendId = backendIdsByBodyRowIndex.remove(rowIndex); + BackendBodyHandle previousHandle = bodyHandlesByRowIndex.remove(rowIndex); + BackendSpaceHandle previousSpaceHandle = bodySpaceHandlesByRowIndex.remove(rowIndex); + bodyRefsByRowIndex.remove(rowIndex); + chunkCollisionPayloadKeysByRowIndex.remove(rowIndex); + removeBodyHandleIndexes(previousBackendId, previousHandle, previousSpaceHandle); + bodyRefsByRowIndex.put(rowIndex, checkedBodyRef); + bodyHandlesByRowIndex.put(rowIndex, Objects.requireNonNull(handle, "handle")); + bodySpaceHandlesByRowIndex.put(rowIndex, Objects.requireNonNull(spaceHandle, "spaceHandle")); + if (backendId != null) { + backendIdsByBodyRowIndex.put(rowIndex, backendId); + bodyHandlesBySpaceKey.computeIfAbsent(new BackendSpaceKey(backendId, spaceHandle), + _ -> new LongArrayList()).add(handle.value()); + } else { + backendIdsByBodyRowIndex.remove(rowIndex); + } + markRegistrationTopologyChanged(); + } + + @Nullable + public BackendBodyHandle getBodyHandle(@Nonnull Ref bodyRef) { + return bodyHandlesByRowIndex.get(bodyRef.getIndex()); + } + + @Nullable + public BackendSpaceHandle getBodySpaceHandle(@Nonnull Ref bodyRef) { + return bodySpaceHandlesByRowIndex.get(bodyRef.getIndex()); + } + + @Nullable + public BackendId getBodyBackendId(@Nonnull Ref bodyRef) { + return backendIdsByBodyRowIndex.get(bodyRef.getIndex()); + } + + public void removeBodyHandle(@Nonnull Ref bodyRef) { + Ref checkedBodyRef = Objects.requireNonNull(bodyRef, "bodyRef"); + removePendingBodyOperations(checkedBodyRef); + int rowIndex = checkedBodyRef.getIndex(); + BackendId backendId = backendIdsByBodyRowIndex.remove(rowIndex); + BackendBodyHandle removed = bodyHandlesByRowIndex.remove(rowIndex); + BackendSpaceHandle spaceHandle = bodySpaceHandlesByRowIndex.remove(rowIndex); + bodyRefsByRowIndex.remove(rowIndex); + chunkCollisionPayloadKeysByRowIndex.remove(rowIndex); + removeBodyHandleIndexes(backendId, removed, spaceHandle); + markRegistrationTopologyChanged(); + } + + public boolean removeBackendBody(@Nonnull UUID bodyUuid, + @Nullable PhysicsBackendRuntime fallbackRuntime) { + BackendBodyKey bodyKey = bodySnapshotKeysByUuid.get( + Objects.requireNonNull(bodyUuid, "bodyUuid")); + if (bodyKey == null) { + return false; + } + BodySnapshotMetadata metadata = bodySnapshotMetadataByKey.get(bodyKey); + if (metadata == null || !bodyUuid.equals(metadata.bodyUuid())) { + return false; + } + PhysicsBackendRuntime runtime = runtimeForBackendId(bodyKey.backendId()); + if (runtime == null) { + runtime = fallbackRuntime; + } + if (runtime != null) { + runtime.removeBody(bodyKey.spaceHandle(), bodyKey.bodyHandle()); + } + removePendingBodyOperations(metadata.bodyRef()); + removeBodyHandleFromSpaceIndex(bodyKey); + removeBodyMetadata(bodyKey); + markRegistrationTopologyChanged(); + return true; + } + + @Nonnull + public List> bodyRefsForSpaceHandle(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle) { + List> bodyRefs = new ArrayList<>(); + BackendSpaceKey target = new BackendSpaceKey(backendId, spaceHandle); + bodySpaceHandlesByRowIndex.forEach((rowIndex, handle) -> { + BackendId rowBackendId = backendIdsByBodyRowIndex.get((int) rowIndex); + if (rowBackendId != null + && rowBackendId.equals(target.backendId()) + && handle.value() == target.spaceHandle()) { + Ref bodyRef = bodyRefsByRowIndex.get((int) rowIndex); + if (bodyRef != null) { + bodyRefs.add(bodyRef); + } + } + }); + return bodyRefs; + } + + public void putBodySnapshotMetadata(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull BackendBodyHandle handle, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef, + @Nonnull UUID spaceUuid) { + BackendBodyKey key = new BackendBodyKey(backendId, spaceHandle, handle); + BodySnapshotMetadata previousMetadata = bodySnapshotMetadataByKey.get(key); + if (previousMetadata != null) { + bodySnapshotKeysByUuid.remove(previousMetadata.bodyUuid(), key); + } + removeBodyMetadataForUuid(bodyUuid, key, bodyRef); + bodySnapshotMetadataByKey.put(key, + new BodySnapshotMetadata(bodyUuid, bodyRef, spaceUuid)); + bodySnapshotKeysByUuid.put(bodyUuid, key); + } + + public void putBodyHitMetadata(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull BackendBodyHandle handle, + @Nullable Ref bodyRef, + @Nonnull PhysicsBodyType bodyType, + @Nonnull ShapeType shapeType) { + BackendBodyKey key = new BackendBodyKey(backendId, spaceHandle, handle); + BodySnapshotMetadata metadata = bodySnapshotMetadataByKey.get(key); + putBodyHitMetadata(backendId, + spaceHandle, + handle, + metadata != null ? metadata.bodyUuid() : new UUID(0L, 0L), + bodyRef, + bodyType, + shapeType); + } + + public void putBodyHitMetadata(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull BackendBodyHandle handle, + @Nonnull UUID bodyUuid, + @Nullable Ref bodyRef, + @Nonnull PhysicsBodyType bodyType, + @Nonnull ShapeType shapeType) { + bodyHitMetadataByKey.put(new BackendBodyKey(backendId, spaceHandle, handle), + new BodyHitMetadata(bodyUuid, bodyRef, bodyType, shapeType)); + } + + @Nullable + public BodyHitMetadata getBodyHitMetadata(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + long bodyHandle) { + return bodyHitMetadataByKey.get(new BackendBodyKey(backendId, spaceHandle, bodyHandle)); + } + + @Nullable + public BodySnapshotMetadata getBodySnapshotMetadata(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + long bodyHandle) { + return bodySnapshotMetadataByKey.get(new BackendBodyKey(backendId, spaceHandle, bodyHandle)); + } + + public void removeBodyHitMetadata(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull BackendBodyHandle handle) { + bodyHitMetadataByKey.remove(new BackendBodyKey(backendId, spaceHandle, handle)); + } + + public void enqueuePendingBodyOperation(@Nonnull PendingBodyOperation operation) { + pendingBodyOperations.add(Objects.requireNonNull(operation, "operation")); + } + + public void markSpaceSettingsPending(@Nonnull Ref spaceRef) { + Ref checkedSpaceRef = Objects.requireNonNull(spaceRef, "spaceRef"); + pendingSpaceSettingsByRowIndex.put(checkedSpaceRef.getIndex(), checkedSpaceRef); + } + + public void clearPendingSpaceSettings(@Nonnull Ref spaceRef) { + pendingSpaceSettingsByRowIndex.remove(Objects.requireNonNull(spaceRef, "spaceRef") + .getIndex()); + } + + @Nonnull + public Set> drainPendingSpaceSettings() { + if (pendingSpaceSettingsByRowIndex.isEmpty()) { + return Set.of(); + } + Set> drained = new ObjectOpenHashSet<>( + pendingSpaceSettingsByRowIndex.values()); + pendingSpaceSettingsByRowIndex.clear(); + return drained; + } + + @Nonnull + public List drainPendingBodyOperations() { + if (pendingBodyOperations.isEmpty()) { + return List.of(); + } + List drained = new ArrayList<>(pendingBodyOperations); + pendingBodyOperations.clear(); + return drained; + } + + public void removePendingBodyOperations(@Nonnull Ref bodyRef) { + Ref checkedBodyRef = Objects.requireNonNull(bodyRef, "bodyRef"); + pendingBodyOperations.removeIf(operation -> + operation.bodyRef().getStore() == checkedBodyRef.getStore() + && operation.bodyRef().getIndex() == checkedBodyRef.getIndex()); + } + + public void putJointHandle(@Nonnull Ref jointRef, + @Nonnull Ref spaceRef, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull BackendJointHandle handle) { + Ref checkedJointRef = Objects.requireNonNull(jointRef, "jointRef"); + BackendId backendId = getSpaceBackendId(Objects.requireNonNull(spaceRef, "spaceRef")); + int rowIndex = checkedJointRef.getIndex(); + BackendJointHandle previousHandle = jointHandlesByRowIndex.remove(rowIndex); + BackendSpaceHandle previousSpaceHandle = jointSpaceHandlesByRowIndex.remove(rowIndex); + BackendId previousBackendId = backendIdsByJointRowIndex.remove(rowIndex); + if (previousBackendId != null && previousSpaceHandle != null && previousHandle != null) { + removeJointMetadata(new BackendJointKey(previousBackendId, + previousSpaceHandle, + previousHandle)); + } + jointRefsByRowIndex.put(rowIndex, checkedJointRef); + jointHandlesByRowIndex.put(rowIndex, Objects.requireNonNull(handle, "handle")); + jointSpaceHandlesByRowIndex.put(rowIndex, Objects.requireNonNull(spaceHandle, "spaceHandle")); + if (backendId != null) { + backendIdsByJointRowIndex.put(rowIndex, backendId); + } + } + + public void putJointMetadata(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull BackendJointHandle handle, + @Nonnull UUID jointUuid, + @Nonnull Ref jointRef) { + BackendJointKey key = new BackendJointKey(backendId, spaceHandle, handle); + JointMetadata previousMetadata = jointMetadataByKey.get(key); + if (previousMetadata != null) { + jointKeysByUuid.remove(previousMetadata.jointUuid(), key); + } + removeJointMetadataForUuid(jointUuid, key, jointRef); + jointMetadataByKey.put(key, + new JointMetadata(jointUuid, jointRef)); + jointKeysByUuid.put(jointUuid, key); + } + + @Nullable + public BackendJointHandle getJointHandle(@Nonnull Ref jointRef) { + return jointHandlesByRowIndex.get(jointRef.getIndex()); + } + + @Nullable + public UUID getJointUuid(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + long jointHandle) { + JointMetadata metadata = jointMetadataByKey.get(new BackendJointKey(backendId, + spaceHandle.value(), + jointHandle)); + return metadata != null ? metadata.jointUuid() : null; + } + + @Nullable + public BackendSpaceHandle getJointSpaceHandle(@Nonnull Ref jointRef) { + return jointSpaceHandlesByRowIndex.get(jointRef.getIndex()); + } + + @Nullable + public BackendId getJointBackendId(@Nonnull Ref jointRef) { + return backendIdsByJointRowIndex.get(jointRef.getIndex()); + } + + public void removeJointHandle(@Nonnull Ref jointRef) { + int rowIndex = Objects.requireNonNull(jointRef, "jointRef").getIndex(); + BackendJointHandle removed = jointHandlesByRowIndex.remove(rowIndex); + BackendSpaceHandle spaceHandle = jointSpaceHandlesByRowIndex.remove(rowIndex); + BackendId backendId = backendIdsByJointRowIndex.remove(rowIndex); + jointRefsByRowIndex.remove(rowIndex); + if (backendId != null && spaceHandle != null && removed != null) { + removeJointMetadata(new BackendJointKey(backendId, spaceHandle, removed)); + } + } + + @Nonnull + public List> jointRefsForSpaceHandle(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle) { + List> jointRefs = new ArrayList<>(); + BackendSpaceKey target = new BackendSpaceKey(backendId, spaceHandle); + jointSpaceHandlesByRowIndex.forEach((rowIndex, handle) -> { + BackendId rowBackendId = backendIdsByJointRowIndex.get((int) rowIndex); + if (rowBackendId != null + && rowBackendId.equals(target.backendId()) + && handle.value() == target.spaceHandle()) { + Ref jointRef = jointRefsByRowIndex.get((int) rowIndex); + if (jointRef != null) { + jointRefs.add(jointRef); + } + } + }); + return jointRefs; + } + + public void markChunkCollisionPayloadBound(@Nonnull Ref bodyRef, + @Nonnull String payloadKey) { + chunkCollisionPayloadKeysByRowIndex.put(bodyRef.getIndex(), payloadKey); + } + + public boolean isChunkCollisionPayloadBound(@Nonnull Ref bodyRef, + @Nonnull String payloadKey) { + return payloadKey.equals(chunkCollisionPayloadKeysByRowIndex.get(bodyRef.getIndex())); + } + + public void clearChunkCollisionPayloadBound(@Nonnull Ref bodyRef) { + chunkCollisionPayloadKeysByRowIndex.remove(bodyRef.getIndex()); + } + + public void forEachRuntimeSpaceBinding(@Nonnull RuntimeSpaceBindingConsumer consumer) { + Objects.requireNonNull(consumer, "consumer"); + spaceRefsByRowIndex.forEach((rowIndex, spaceRef) -> { + BackendSpaceHandle spaceHandle = spaceHandlesByRowIndex.get((int) rowIndex); + BackendId backendId = backendIdsBySpaceRowIndex.get((int) rowIndex); + PhysicsBackendRuntime runtime = backendId != null ? runtimesByBackend.get(backendId) : null; + if (spaceHandle != null && backendId != null && runtime != null) { + consumer.accept(spaceRef, backendId, spaceHandle, runtime); + } + }); + } + + public void forEachBodyHandle(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull LongConsumer consumer) { + LongList bodyHandles = bodyHandlesBySpaceKey.get(new BackendSpaceKey(backendId, spaceHandle)); + if (bodyHandles == null) { + return; + } + bodyHandles.forEach(consumer); + } + + public int bodyHandleCount(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle) { + LongList bodyHandles = bodyHandlesBySpaceKey.get(new BackendSpaceKey(backendId, spaceHandle)); + return bodyHandles != null ? bodyHandles.size() : 0; + } + + public void clear() { + runtimesByBackend.clear(); + spaceRefsByRowIndex.clear(); + spaceHandlesByRowIndex.clear(); + backendIdsBySpaceRowIndex.clear(); + spaceMetadataByKey.clear(); + bodyRefsByRowIndex.clear(); + bodyHandlesByRowIndex.clear(); + bodySpaceHandlesByRowIndex.clear(); + backendIdsByBodyRowIndex.clear(); + jointHandlesByRowIndex.clear(); + jointSpaceHandlesByRowIndex.clear(); + jointRefsByRowIndex.clear(); + backendIdsByJointRowIndex.clear(); + jointMetadataByKey.clear(); + jointKeysByUuid.clear(); + chunkCollisionPayloadKeysByRowIndex.clear(); + bodyHandlesBySpaceKey.clear(); + bodyHitMetadataByKey.clear(); + bodySnapshotMetadataByKey.clear(); + bodySnapshotKeysByUuid.clear(); + pendingBodyOperations.clear(); + pendingSpaceSettingsByRowIndex.clear(); + started = false; + markRegistrationTopologyChanged(); + } + + public void clearTransientBodyOperations() { + pendingBodyOperations.clear(); + } + + public long registrationTopologyGeneration() { + return registrationTopologyGeneration; + } + + private void markRegistrationTopologyChanged() { + registrationTopologyGeneration++; + } + + public void refreshRowRefs(@Nonnull PhysicsStore physicsStore) { + PhysicsStore checkedPhysicsStore = Objects.requireNonNull(physicsStore, "physicsStore"); + refreshSpaceRefs(checkedPhysicsStore); + refreshBodyRefs(checkedPhysicsStore); + refreshJointRefs(checkedPhysicsStore); + } + + private void refreshSpaceRefs(@Nonnull PhysicsStore physicsStore) { + spaceRefsByRowIndex.clear(); + spaceHandlesByRowIndex.clear(); + backendIdsBySpaceRowIndex.clear(); + spaceMetadataByKey.replaceAll((key, metadata) -> { + Ref spaceRef = physicsStore.getRefFromUUID(metadata.spaceUuid()); + if (spaceRef == null) { + return metadata; + } + int rowIndex = spaceRef.getIndex(); + spaceRefsByRowIndex.put(rowIndex, spaceRef); + spaceHandlesByRowIndex.put(rowIndex, new BackendSpaceHandle(key.spaceHandle())); + backendIdsBySpaceRowIndex.put(rowIndex, key.backendId()); + return new SpaceMetadata(metadata.spaceUuid(), spaceRef); + }); + } + + private void refreshBodyRefs(@Nonnull PhysicsStore physicsStore) { + bodyRefsByRowIndex.clear(); + bodyHandlesByRowIndex.clear(); + bodySpaceHandlesByRowIndex.clear(); + backendIdsByBodyRowIndex.clear(); + bodySnapshotMetadataByKey.replaceAll((key, metadata) -> { + Ref bodyRef = physicsStore.getRefFromUUID(metadata.bodyUuid()); + if (bodyRef == null) { + return metadata; + } + int rowIndex = bodyRef.getIndex(); + bodyRefsByRowIndex.put(rowIndex, bodyRef); + bodyHandlesByRowIndex.put(rowIndex, new BackendBodyHandle(key.bodyHandle())); + bodySpaceHandlesByRowIndex.put(rowIndex, new BackendSpaceHandle(key.spaceHandle())); + backendIdsByBodyRowIndex.put(rowIndex, key.backendId()); + return new BodySnapshotMetadata(metadata.bodyUuid(), bodyRef, metadata.spaceUuid()); + }); + bodyHitMetadataByKey.replaceAll((_, metadata) -> { + Ref bodyRef = physicsStore.getRefFromUUID(metadata.bodyUuid()); + if (bodyRef == null) { + return metadata; + } + return new BodyHitMetadata(metadata.bodyUuid(), + bodyRef, + metadata.bodyType(), + metadata.shapeType()); + }); + } + + private void refreshJointRefs(@Nonnull PhysicsStore physicsStore) { + jointHandlesByRowIndex.clear(); + jointSpaceHandlesByRowIndex.clear(); + jointRefsByRowIndex.clear(); + backendIdsByJointRowIndex.clear(); + jointMetadataByKey.replaceAll((key, metadata) -> { + Ref jointRef = physicsStore.getRefFromUUID(metadata.jointUuid()); + if (jointRef == null) { + return metadata; + } + int rowIndex = jointRef.getIndex(); + jointRefsByRowIndex.put(rowIndex, jointRef); + jointHandlesByRowIndex.put(rowIndex, new BackendJointHandle(key.jointHandle())); + jointSpaceHandlesByRowIndex.put(rowIndex, new BackendSpaceHandle(key.spaceHandle())); + backendIdsByJointRowIndex.put(rowIndex, key.backendId()); + return new JointMetadata(metadata.jointUuid(), jointRef); + }); + } + + private void removeBodyHandleIndexes(@Nullable BackendId backendId, + @Nullable BackendBodyHandle removed, + @Nullable BackendSpaceHandle spaceHandle) { + if (backendId == null || removed == null || spaceHandle == null) { + return; + } + BackendBodyKey bodyKey = new BackendBodyKey(backendId, spaceHandle, removed); + removeBodyHandleFromSpaceIndex(bodyKey); + removeBodyMetadata(bodyKey); + } + + private void removeBodyHandleFromSpaceIndex(@Nonnull BackendBodyKey key) { + BackendSpaceKey spaceKey = new BackendSpaceKey(key.backendId(), key.spaceHandle()); + LongList bodyHandles = bodyHandlesBySpaceKey.get(spaceKey); + if (bodyHandles != null) { + bodyHandles.rem(key.bodyHandle()); + if (bodyHandles.isEmpty()) { + bodyHandlesBySpaceKey.remove(spaceKey); + } + } + } + + private void removeBodyMetadata(@Nonnull BackendBodyKey key) { + bodyHitMetadataByKey.remove(key); + BodySnapshotMetadata metadata = bodySnapshotMetadataByKey.remove(key); + if (metadata == null) { + return; + } + bodySnapshotKeysByUuid.remove(metadata.bodyUuid(), key); + int rowIndex = metadata.bodyRef().getIndex(); + bodyRefsByRowIndex.remove(rowIndex); + bodyHandlesByRowIndex.remove(rowIndex); + bodySpaceHandlesByRowIndex.remove(rowIndex); + backendIdsByBodyRowIndex.remove(rowIndex); + chunkCollisionPayloadKeysByRowIndex.remove(rowIndex); + } + + private void removeBodyMetadataForUuid(@Nonnull UUID bodyUuid, + @Nonnull BackendBodyKey replacementKey, + @Nonnull Ref replacementRef) { + BackendBodyKey removedKey = bodySnapshotKeysByUuid.get(bodyUuid); + if (removedKey == null || removedKey.equals(replacementKey)) { + return; + } + removeBodyHandleFromSpaceIndex(removedKey); + bodyHitMetadataByKey.remove(removedKey); + BodySnapshotMetadata metadata = bodySnapshotMetadataByKey.remove(removedKey); + bodySnapshotKeysByUuid.remove(bodyUuid, removedKey); + if (metadata != null && !sameRow(metadata.bodyRef(), replacementRef)) { + int rowIndex = metadata.bodyRef().getIndex(); + bodyRefsByRowIndex.remove(rowIndex); + bodyHandlesByRowIndex.remove(rowIndex); + bodySpaceHandlesByRowIndex.remove(rowIndex); + backendIdsByBodyRowIndex.remove(rowIndex); + chunkCollisionPayloadKeysByRowIndex.remove(rowIndex); + } + } + + private void removeJointMetadata(@Nonnull BackendJointKey key) { + JointMetadata metadata = jointMetadataByKey.remove(key); + if (metadata == null) { + return; + } + jointKeysByUuid.remove(metadata.jointUuid(), key); + int rowIndex = metadata.jointRef().getIndex(); + jointHandlesByRowIndex.remove(rowIndex); + jointSpaceHandlesByRowIndex.remove(rowIndex); + jointRefsByRowIndex.remove(rowIndex); + backendIdsByJointRowIndex.remove(rowIndex); + } + + private void removeJointMetadataForUuid(@Nonnull UUID jointUuid, + @Nonnull BackendJointKey replacementKey, + @Nonnull Ref replacementRef) { + BackendJointKey removedKey = jointKeysByUuid.get(jointUuid); + if (removedKey == null || removedKey.equals(replacementKey)) { + return; + } + JointMetadata metadata = jointMetadataByKey.remove(removedKey); + jointKeysByUuid.remove(jointUuid, removedKey); + if (metadata != null && !sameRow(metadata.jointRef(), replacementRef)) { + int rowIndex = metadata.jointRef().getIndex(); + jointHandlesByRowIndex.remove(rowIndex); + jointSpaceHandlesByRowIndex.remove(rowIndex); + jointRefsByRowIndex.remove(rowIndex); + backendIdsByJointRowIndex.remove(rowIndex); + } + } + + private static boolean sameRow(@Nonnull Ref first, + @Nonnull Ref second) { + return first.getStore() == second.getStore() && first.getIndex() == second.getIndex(); + } + + public void destroyBackendBindings() { + RuntimeException failure = null; + for (Map.Entry entry + : new ArrayList<>(jointMetadataByKey.entrySet())) { + PhysicsBackendRuntime runtime = runtimesByBackend.get(entry.getKey().backendId()); + if (runtime == null) { + continue; + } + try { + runtime.removeJoint(entry.getKey().spaceHandle(), entry.getKey().jointHandle()); + } catch (RuntimeException exception) { + failure = appendShutdownFailure(failure, exception); + } + } + for (Map.Entry entry + : new ArrayList<>(bodySnapshotMetadataByKey.entrySet())) { + PhysicsBackendRuntime runtime = runtimesByBackend.get(entry.getKey().backendId()); + if (runtime == null) { + continue; + } + try { + runtime.removeBody(entry.getKey().spaceHandle(), entry.getKey().bodyHandle()); + } catch (RuntimeException exception) { + failure = appendShutdownFailure(failure, exception); + } + } + for (BackendSpaceKey key : new ArrayList<>(spaceMetadataByKey.keySet())) { + PhysicsBackendRuntime runtime = runtimesByBackend.get(key.backendId()); + if (runtime == null) { + continue; + } + try { + runtime.destroySpace(key.spaceHandle()); + } catch (RuntimeException exception) { + failure = appendShutdownFailure(failure, exception); + } + } + for (PhysicsBackendRuntime runtime : new ArrayList<>(runtimesByBackend.values())) { + try { + runtime.close(); + } catch (RuntimeException exception) { + failure = appendShutdownFailure(failure, exception); + } + } + clear(); + if (failure != null) { + throw failure; + } + } + + @Nullable + public PhysicsBackendRuntime runtimeForSpaceRef(@Nonnull Ref spaceRef) { + return runtimeForBackendId(backendIdsBySpaceRowIndex.get( + Objects.requireNonNull(spaceRef, "spaceRef").getIndex())); + } + + @Nullable + public PhysicsBackendRuntime runtimeForBodyRef(@Nonnull Ref bodyRef) { + return runtimeForBackendId(backendIdsByBodyRowIndex.get( + Objects.requireNonNull(bodyRef, "bodyRef").getIndex())); + } + + @Nullable + public PhysicsBackendRuntime runtimeForJointRef(@Nonnull Ref jointRef) { + return runtimeForBackendId(backendIdsByJointRowIndex.get( + Objects.requireNonNull(jointRef, "jointRef").getIndex())); + } + + @Nullable + private PhysicsBackendRuntime runtimeForBackendId(@Nullable BackendId backendId) { + return backendId != null ? runtimesByBackend.get(backendId) : null; + } + + @Nullable + private BackendSpaceKey spaceKey(@Nonnull Ref spaceRef) { + int rowIndex = Objects.requireNonNull(spaceRef, "spaceRef").getIndex(); + BackendId backendId = backendIdsBySpaceRowIndex.get(rowIndex); + BackendSpaceHandle handle = spaceHandlesByRowIndex.get(rowIndex); + return backendId != null && handle != null ? new BackendSpaceKey(backendId, handle) : null; + } + + @Nonnull + private static RuntimeException appendShutdownFailure(@Nullable RuntimeException failure, + @Nonnull RuntimeException exception) { + if (failure == null) { + return exception; + } + failure.addSuppressed(exception); + return failure; + } + + @Nonnull + @Override + public PhysicsRuntimeResource clone() { + PhysicsRuntimeResource copy = new PhysicsRuntimeResource(); + copy.runtimesByBackend.putAll(runtimesByBackend); + copy.spaceRefsByRowIndex.putAll(spaceRefsByRowIndex); + copy.spaceHandlesByRowIndex.putAll(spaceHandlesByRowIndex); + copy.backendIdsBySpaceRowIndex.putAll(backendIdsBySpaceRowIndex); + copy.spaceMetadataByKey.putAll(spaceMetadataByKey); + copy.bodyRefsByRowIndex.putAll(bodyRefsByRowIndex); + copy.bodyHandlesByRowIndex.putAll(bodyHandlesByRowIndex); + copy.bodySpaceHandlesByRowIndex.putAll(bodySpaceHandlesByRowIndex); + copy.backendIdsByBodyRowIndex.putAll(backendIdsByBodyRowIndex); + copy.jointHandlesByRowIndex.putAll(jointHandlesByRowIndex); + copy.jointSpaceHandlesByRowIndex.putAll(jointSpaceHandlesByRowIndex); + copy.jointRefsByRowIndex.putAll(jointRefsByRowIndex); + copy.backendIdsByJointRowIndex.putAll(backendIdsByJointRowIndex); + copy.jointMetadataByKey.putAll(jointMetadataByKey); + copy.jointKeysByUuid.putAll(jointKeysByUuid); + copy.chunkCollisionPayloadKeysByRowIndex.putAll(chunkCollisionPayloadKeysByRowIndex); + bodyHandlesBySpaceKey.forEach((key, bodyHandles) -> + copy.bodyHandlesBySpaceKey.put(key, new LongArrayList(bodyHandles))); + copy.bodyHitMetadataByKey.putAll(bodyHitMetadataByKey); + copy.bodySnapshotMetadataByKey.putAll(bodySnapshotMetadataByKey); + copy.bodySnapshotKeysByUuid.putAll(bodySnapshotKeysByUuid); + copy.pendingBodyOperations.addAll(pendingBodyOperations); + copy.pendingSpaceSettingsByRowIndex.putAll(pendingSpaceSettingsByRowIndex); + copy.registrationTopologyGeneration = registrationTopologyGeneration; + copy.started = started; + return copy; + } + + @Nonnull + public static ResourceType getResourceType() { + return PhysicsResourceTypes.runtimeResourceType(); + } + + @FunctionalInterface + public interface RuntimeSpaceBindingConsumer { + + void accept(@Nonnull Ref spaceRef, + @Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull PhysicsBackendRuntime runtime); + } + + private record BackendSpaceKey(@Nonnull BackendId backendId, int spaceHandle) { + + private BackendSpaceKey(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle) { + this(backendId, spaceHandle.value()); + } + + private BackendSpaceKey { + Objects.requireNonNull(backendId, "backendId"); + } + } + + private record BackendBodyKey(@Nonnull BackendId backendId, + int spaceHandle, + long bodyHandle) { + + private BackendBodyKey(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull BackendBodyHandle bodyHandle) { + this(backendId, spaceHandle.value(), bodyHandle.value()); + } + + private BackendBodyKey(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + long bodyHandle) { + this(backendId, spaceHandle.value(), bodyHandle); + } + + private BackendBodyKey { + Objects.requireNonNull(backendId, "backendId"); + } + } + + private record BackendJointKey(@Nonnull BackendId backendId, + int spaceHandle, + long jointHandle) { + + private BackendJointKey(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull BackendJointHandle jointHandle) { + this(backendId, spaceHandle.value(), jointHandle.value()); + } + + private BackendJointKey { + Objects.requireNonNull(backendId, "backendId"); + } + } + + private record SpaceMetadata(@Nonnull UUID spaceUuid, + @Nonnull Ref spaceRef) { + + private SpaceMetadata { + Objects.requireNonNull(spaceUuid, "spaceUuid"); + Objects.requireNonNull(spaceRef, "spaceRef"); + } + } + + private record JointMetadata(@Nonnull UUID jointUuid, + @Nonnull Ref jointRef) { + + private JointMetadata { + Objects.requireNonNull(jointUuid, "jointUuid"); + Objects.requireNonNull(jointRef, "jointRef"); + } + } + + public record BodyHitMetadata(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef, + @Nonnull PhysicsBodyType bodyType, + @Nonnull ShapeType shapeType) { + + public BodyHitMetadata { + Objects.requireNonNull(bodyUuid, "bodyUuid"); + Objects.requireNonNull(bodyType, "bodyType"); + Objects.requireNonNull(shapeType, "shapeType"); + } + } + + public record BodySnapshotMetadata(@Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef, + @Nonnull UUID spaceUuid) { + + public BodySnapshotMetadata { + Objects.requireNonNull(bodyUuid, "bodyUuid"); + Objects.requireNonNull(bodyRef, "bodyRef"); + Objects.requireNonNull(spaceUuid, "spaceUuid"); + } + } + + public record PendingBodyOperation(@Nonnull Kind kind, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef, + float x, + float y, + float z, + boolean hasOffset, + float offsetX, + float offsetY, + float offsetZ) { + + public PendingBodyOperation { + Objects.requireNonNull(kind, "kind"); + Objects.requireNonNull(bodyUuid, "bodyUuid"); + Objects.requireNonNull(bodyRef, "bodyRef"); + } + + @Nonnull + public static PendingBodyOperation wake(@Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef) { + return empty(Kind.WAKE, bodyUuid, bodyRef); + } + + @Nonnull + public static PendingBodyOperation sleep(@Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef) { + return empty(Kind.SLEEP, bodyUuid, bodyRef); + } + + @Nonnull + public static PendingBodyOperation vector(@Nonnull Kind kind, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef, + float x, + float y, + float z, + boolean hasOffset, + float offsetX, + float offsetY, + float offsetZ) { + return new PendingBodyOperation(kind, + bodyUuid, + bodyRef, + x, + y, + z, + hasOffset, + offsetX, + offsetY, + offsetZ); + } + + @Nonnull + private static PendingBodyOperation empty(@Nonnull Kind kind, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef) { + return new PendingBodyOperation(kind, + bodyUuid, + bodyRef, + 0.0f, + 0.0f, + 0.0f, + false, + 0.0f, + 0.0f, + 0.0f); + } + + public enum Kind { + WAKE, + SLEEP, + IMPULSE, + TORQUE_IMPULSE, + FORCE, + TORQUE + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSimulationRuntime.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSimulationRuntime.java deleted file mode 100644 index 9d2d6f20..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSimulationRuntime.java +++ /dev/null @@ -1,26 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources; - -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import javax.annotation.Nonnull; - -/** - * World-level simulation policy for one runtime physics resource. - */ -public final class PhysicsSimulationRuntime { - - @Nonnull - private final PhysicsWorldSettings worldSettings = new PhysicsWorldSettings(); - - @Nonnull - public PhysicsWorldSettings getWorldSettings() { - return new PhysicsWorldSettings(worldSettings); - } - - public void setWorldSettings(@Nonnull PhysicsWorldSettings settings) { - worldSettings.copyFrom(settings); - } - - public void copyFrom(@Nonnull PhysicsSimulationRuntime other) { - worldSettings.copyFrom(other.worldSettings); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSnapshotResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSnapshotResource.java new file mode 100644 index 00000000..cbb8a696 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSnapshotResource.java @@ -0,0 +1,677 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsSnapshotFrame; +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Latest copied PhysicsStore snapshot frame for projection, debug, and queries. + */ +public final class PhysicsSnapshotResource implements Resource { + + @Nonnull + private volatile CompactSnapshot snapshot = CompactSnapshot.EMPTY; + + public PhysicsSnapshotResource() { + } + + @Nonnull + public PhysicsSnapshotFrame getLatestFrame() { + return snapshot.frame(); + } + + public long latestSequence() { + return snapshot.sequence(); + } + + public int bodyCount() { + return snapshot.bodyCount(); + } + + /** + * Iterates the compact published body frame without materializing snapshot objects. + * + *

The cursor instance is reused during iteration. Consumers must read the needed values + * synchronously and must not retain the cursor after the callback returns.

+ */ + public void forEachBodyCursor(@Nonnull Consumer consumer) { + snapshot.forEachBodyCursor(Objects.requireNonNull(consumer, "consumer")); + } + + @Nullable + public PhysicsBodySnapshot getBody(@Nonnull UUID bodyUuid) { + return snapshot.body(Objects.requireNonNull(bodyUuid, "bodyUuid")); + } + + public boolean containsBody(@Nonnull UUID bodyUuid) { + return snapshot.containsBody(Objects.requireNonNull(bodyUuid, "bodyUuid")); + } + + @Nullable + public PhysicsBodySnapshot getBody(@Nonnull Ref bodyRef) { + return snapshot.body(Objects.requireNonNull(bodyRef, "bodyRef")); + } + + public void publish(@Nonnull PhysicsSnapshotFrame frame) { + snapshot = CompactSnapshot.fromFrame(Objects.requireNonNull(frame, "frame")); + } + + public void publish(long sequence, + float dt, + @Nonnull CompactSnapshot compactSnapshot) { + snapshot = Objects.requireNonNull(compactSnapshot, "compactSnapshot") + .withFrame(sequence, dt); + } + + public void removeBody(@Nonnull UUID bodyUuid) { + Objects.requireNonNull(bodyUuid, "bodyUuid"); + removeBodies(List.of(bodyUuid)); + } + + public void removeBodies(@Nonnull Collection bodyUuids) { + Objects.requireNonNull(bodyUuids, "bodyUuids"); + CompactSnapshot current = snapshot; + if (bodyUuids.isEmpty()) { + return; + } + ObjectOpenHashSet removedBodyUuids = new ObjectOpenHashSet<>(bodyUuids.size()); + for (UUID bodyUuid : bodyUuids) { + Objects.requireNonNull(bodyUuid, "bodyUuid"); + if (current.containsBody(bodyUuid)) { + removedBodyUuids.add(bodyUuid); + } + } + if (removedBodyUuids.isEmpty()) { + return; + } + snapshot = withoutBodies(current, removedBodyUuids); + } + + public void clear() { + snapshot = CompactSnapshot.EMPTY; + } + + @Nonnull + private static CompactSnapshot withoutBodies(@Nonnull CompactSnapshot current, + @Nonnull ObjectOpenHashSet bodyUuids) { + int bodyCount = Math.max(0, current.bodyCount() - bodyUuids.size()); + CompactSnapshotBuilder builder = compactBuilder(bodyCount); + for (int index = 0; index < current.bodyCount(); index++) { + if (bodyUuids.contains(current.bodyUuids[index])) { + continue; + } + builder.addBody(current.bodyRefs[index], + current.bodyUuids[index], + current.spaceUuids[index], + current.bodyTypes[index], + current.value(index, CompactSnapshot.POSITION_X), + current.value(index, CompactSnapshot.POSITION_Y), + current.value(index, CompactSnapshot.POSITION_Z), + current.value(index, CompactSnapshot.ROTATION_X), + current.value(index, CompactSnapshot.ROTATION_Y), + current.value(index, CompactSnapshot.ROTATION_Z), + current.value(index, CompactSnapshot.ROTATION_W), + current.value(index, CompactSnapshot.LINEAR_VELOCITY_X), + current.value(index, CompactSnapshot.LINEAR_VELOCITY_Y), + current.value(index, CompactSnapshot.LINEAR_VELOCITY_Z), + current.value(index, CompactSnapshot.ANGULAR_VELOCITY_X), + current.value(index, CompactSnapshot.ANGULAR_VELOCITY_Y), + current.value(index, CompactSnapshot.ANGULAR_VELOCITY_Z), + current.value(index, CompactSnapshot.CENTER_OF_MASS_OFFSET_Y), + current.sleeping.get(index)); + } + return builder.build(current.sequence(), current.dt()); + } + + @Nonnull + @Override + public PhysicsSnapshotResource clone() { + PhysicsSnapshotResource copy = new PhysicsSnapshotResource(); + copy.snapshot = snapshot; + return copy; + } + + @Nonnull + public static ResourceType getResourceType() { + return PhysicsResourceTypes.snapshotResourceType(); + } + + @Nonnull + public static CompactSnapshotBuilder compactBuilder(int expectedBodies) { + return new CompactSnapshotBuilder(expectedBodies); + } + + @Nonnull + public static CompactSnapshot emptyCompactSnapshot() { + return CompactSnapshot.EMPTY; + } + + public static final class CompactSnapshot { + + private static final int POSITION_X = 0; + private static final int POSITION_Y = 1; + private static final int POSITION_Z = 2; + private static final int ROTATION_X = 3; + private static final int ROTATION_Y = 4; + private static final int ROTATION_Z = 5; + private static final int ROTATION_W = 6; + private static final int LINEAR_VELOCITY_X = 7; + private static final int LINEAR_VELOCITY_Y = 8; + private static final int LINEAR_VELOCITY_Z = 9; + private static final int ANGULAR_VELOCITY_X = 10; + private static final int ANGULAR_VELOCITY_Y = 11; + private static final int ANGULAR_VELOCITY_Z = 12; + private static final int CENTER_OF_MASS_OFFSET_Y = 13; + private static final int FLOAT_STRIDE = 14; + + private static final CompactSnapshot EMPTY = empty(); + + private final long sequence; + private final float dt; + @Nonnull + private final Ref[] bodyRefs; + @Nonnull + private final UUID[] bodyUuids; + @Nonnull + private final UUID[] spaceUuids; + @Nonnull + private final PhysicsBodyType[] bodyTypes; + @Nonnull + private final float[] values; + @Nonnull + private final BitSet sleeping; + @Nonnull + private final Object2IntOpenHashMap bodiesByUuid; + @Nonnull + private final Int2IntOpenHashMap bodiesByRowIndex; + + private CompactSnapshot(long sequence, + float dt, + @Nonnull Ref[] bodyRefs, + @Nonnull UUID[] bodyUuids, + @Nonnull UUID[] spaceUuids, + @Nonnull PhysicsBodyType[] bodyTypes, + @Nonnull float[] values, + @Nonnull BitSet sleeping, + @Nonnull Object2IntOpenHashMap bodiesByUuid, + @Nonnull Int2IntOpenHashMap bodiesByRowIndex) { + this.sequence = Math.max(0L, sequence); + this.dt = Float.isFinite(dt) ? Math.max(0.0f, dt) : 0.0f; + this.bodyRefs = Objects.requireNonNull(bodyRefs, "bodyRefs"); + this.bodyUuids = Objects.requireNonNull(bodyUuids, "bodyUuids"); + this.spaceUuids = Objects.requireNonNull(spaceUuids, "spaceUuids"); + this.bodyTypes = Objects.requireNonNull(bodyTypes, "bodyTypes"); + this.values = Objects.requireNonNull(values, "values"); + this.sleeping = Objects.requireNonNull(sleeping, "sleeping"); + this.bodiesByUuid = Objects.requireNonNull(bodiesByUuid, "bodiesByUuid"); + this.bodiesByRowIndex = Objects.requireNonNull(bodiesByRowIndex, "bodiesByRowIndex"); + } + + @Nonnull + private static CompactSnapshot empty() { + return new CompactSnapshot(PhysicsSnapshotFrame.EMPTY.sequence(), + PhysicsSnapshotFrame.EMPTY.dt(), + emptyRefs(), + new UUID[0], + new UUID[0], + new PhysicsBodyType[0], + new float[0], + new BitSet(0), + uuidIndex(0), + rowIndex(0)); + } + + @SuppressWarnings("unchecked") + @Nonnull + private static Ref[] emptyRefs() { + return (Ref[]) new Ref[0]; + } + + @Nonnull + private static CompactSnapshot fromFrame(@Nonnull PhysicsSnapshotFrame frame) { + CompactSnapshotBuilder builder = compactBuilder(frame.bodies().size()); + for (PhysicsBodySnapshot body : frame.bodies()) { + builder.addBody(body.bodyRef(), + body.bodyUuid(), + body.spaceUuid(), + body.bodyType(), + body.positionX(), + body.positionY(), + body.positionZ(), + body.rotationX(), + body.rotationY(), + body.rotationZ(), + body.rotationW(), + body.linearVelocityX(), + body.linearVelocityY(), + body.linearVelocityZ(), + body.angularVelocityX(), + body.angularVelocityY(), + body.angularVelocityZ(), + body.centerOfMassOffsetY(), + body.sleeping()); + } + return builder.build(frame.sequence(), frame.dt()); + } + + @Nonnull + private static Object2IntOpenHashMap uuidIndex(int expected) { + Object2IntOpenHashMap index = new Object2IntOpenHashMap<>(expected); + index.defaultReturnValue(-1); + return index; + } + + @Nonnull + private static Int2IntOpenHashMap rowIndex(int expected) { + Int2IntOpenHashMap index = new Int2IntOpenHashMap(expected); + index.defaultReturnValue(-1); + return index; + } + + @Nonnull + private CompactSnapshot withFrame(long sequence, float dt) { + if (bodyCount() == 0 + && sequence == PhysicsSnapshotFrame.EMPTY.sequence() + && dt == PhysicsSnapshotFrame.EMPTY.dt()) { + return EMPTY; + } + return new CompactSnapshot(sequence, + dt, + bodyRefs, + bodyUuids, + spaceUuids, + bodyTypes, + values, + sleeping, + bodiesByUuid, + bodiesByRowIndex); + } + + private long sequence() { + return sequence; + } + + private float dt() { + return dt; + } + + @Nonnull + private PhysicsSnapshotFrame frame() { + if (bodyCount() == 0 && sequence == PhysicsSnapshotFrame.EMPTY.sequence() + && dt == PhysicsSnapshotFrame.EMPTY.dt()) { + return PhysicsSnapshotFrame.EMPTY; + } + List bodies = new java.util.ArrayList<>(bodyCount()); + for (int index = 0; index < bodyCount(); index++) { + bodies.add(body(index)); + } + return new PhysicsSnapshotFrame(sequence, dt, bodies); + } + + public int bodyCount() { + return bodyUuids.length; + } + + private boolean containsBody(@Nonnull UUID bodyUuid) { + return bodiesByUuid.getInt(bodyUuid) >= 0; + } + + @Nullable + private PhysicsBodySnapshot body(@Nonnull UUID bodyUuid) { + int index = bodiesByUuid.getInt(bodyUuid); + return index >= 0 ? body(index) : null; + } + + @Nullable + private PhysicsBodySnapshot body(@Nonnull Ref bodyRef) { + int index = bodiesByRowIndex.get(bodyRef.getIndex()); + if (index < 0 || !sameRef(bodyRefs[index], bodyRef)) { + return null; + } + return body(index); + } + + @Nonnull + private PhysicsBodySnapshot body(int index) { + return PhysicsBodySnapshot.of(bodyRefs[index], + bodyUuids[index], + spaceUuids[index], + bodyTypes[index], + value(index, POSITION_X), + value(index, POSITION_Y), + value(index, POSITION_Z), + value(index, ROTATION_X), + value(index, ROTATION_Y), + value(index, ROTATION_Z), + value(index, ROTATION_W), + value(index, LINEAR_VELOCITY_X), + value(index, LINEAR_VELOCITY_Y), + value(index, LINEAR_VELOCITY_Z), + value(index, ANGULAR_VELOCITY_X), + value(index, ANGULAR_VELOCITY_Y), + value(index, ANGULAR_VELOCITY_Z), + value(index, CENTER_OF_MASS_OFFSET_Y), + sleeping.get(index)); + } + + private void forEachBodyCursor(@Nonnull Consumer consumer) { + PublishedBodyCursor cursor = new PublishedBodyCursor(this); + for (int index = 0; index < bodyCount(); index++) { + cursor.index = index; + consumer.accept(cursor); + } + } + + private float value(int bodyIndex, int valueIndex) { + return values[bodyIndex * FLOAT_STRIDE + valueIndex]; + } + } + + public static final class CompactSnapshotBuilder { + + @Nonnull + private Ref[] bodyRefs; + @Nonnull + private UUID[] bodyUuids; + @Nonnull + private UUID[] spaceUuids; + @Nonnull + private PhysicsBodyType[] bodyTypes; + @Nonnull + private float[] values; + @Nonnull + private BitSet sleeping; + private int size; + + @SuppressWarnings("unchecked") + private CompactSnapshotBuilder(int expectedBodies) { + int capacity = Math.max(0, expectedBodies); + bodyRefs = (Ref[]) new Ref[capacity]; + bodyUuids = new UUID[capacity]; + spaceUuids = new UUID[capacity]; + bodyTypes = new PhysicsBodyType[capacity]; + values = new float[capacity * CompactSnapshot.FLOAT_STRIDE]; + sleeping = new BitSet(capacity); + } + + public void addBody(@Nullable Ref bodyRef, + @Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsBodyType bodyType, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW, + float linearVelocityX, + float linearVelocityY, + float linearVelocityZ, + float angularVelocityX, + float angularVelocityY, + float angularVelocityZ, + float centerOfMassOffsetY, + boolean sleeping) { + ensureCapacity(size + 1); + int index = size++; + bodyRefs[index] = bodyRef; + bodyUuids[index] = Objects.requireNonNull(bodyUuid, "bodyUuid"); + spaceUuids[index] = Objects.requireNonNull(spaceUuid, "spaceUuid"); + bodyTypes[index] = Objects.requireNonNull(bodyType, "bodyType"); + this.sleeping.set(index, sleeping); + put(index, CompactSnapshot.POSITION_X, positionX); + put(index, CompactSnapshot.POSITION_Y, positionY); + put(index, CompactSnapshot.POSITION_Z, positionZ); + put(index, CompactSnapshot.ROTATION_X, rotationX); + put(index, CompactSnapshot.ROTATION_Y, rotationY); + put(index, CompactSnapshot.ROTATION_Z, rotationZ); + put(index, CompactSnapshot.ROTATION_W, rotationW); + put(index, CompactSnapshot.LINEAR_VELOCITY_X, linearVelocityX); + put(index, CompactSnapshot.LINEAR_VELOCITY_Y, linearVelocityY); + put(index, CompactSnapshot.LINEAR_VELOCITY_Z, linearVelocityZ); + put(index, CompactSnapshot.ANGULAR_VELOCITY_X, angularVelocityX); + put(index, CompactSnapshot.ANGULAR_VELOCITY_Y, angularVelocityY); + put(index, CompactSnapshot.ANGULAR_VELOCITY_Z, angularVelocityZ); + put(index, CompactSnapshot.CENTER_OF_MASS_OFFSET_Y, centerOfMassOffsetY); + } + + @Nonnull + public CompactSnapshot build() { + return build(PhysicsSnapshotFrame.EMPTY.sequence(), PhysicsSnapshotFrame.EMPTY.dt()); + } + + @Nonnull + private CompactSnapshot build(long sequence, float dt) { + if (size == 0) { + return CompactSnapshot.EMPTY.withFrame(sequence, dt); + } + Ref[] builtBodyRefs = trimRefs(bodyRefs, size); + UUID[] builtBodyUuids = Arrays.copyOf(bodyUuids, size); + UUID[] builtSpaceUuids = Arrays.copyOf(spaceUuids, size); + PhysicsBodyType[] builtBodyTypes = Arrays.copyOf(bodyTypes, size); + float[] builtValues = Arrays.copyOf(values, size * CompactSnapshot.FLOAT_STRIDE); + BitSet builtSleeping = sleeping.get(0, size); + Object2IntOpenHashMap bodiesByUuid = CompactSnapshot.uuidIndex(size); + Int2IntOpenHashMap bodiesByRowIndex = CompactSnapshot.rowIndex(size); + for (int index = 0; index < size; index++) { + bodiesByUuid.put(builtBodyUuids[index], index); + Ref bodyRef = builtBodyRefs[index]; + if (bodyRef != null) { + bodiesByRowIndex.put(bodyRef.getIndex(), index); + } + } + return new CompactSnapshot(sequence, + dt, + builtBodyRefs, + builtBodyUuids, + builtSpaceUuids, + builtBodyTypes, + builtValues, + builtSleeping, + bodiesByUuid, + bodiesByRowIndex); + } + + private void put(int bodyIndex, int valueIndex, float value) { + values[bodyIndex * CompactSnapshot.FLOAT_STRIDE + valueIndex] = value; + } + + private void ensureCapacity(int required) { + if (required <= bodyUuids.length) { + return; + } + int nextCapacity = Math.max(required, + Math.max(2, bodyUuids.length + (bodyUuids.length >> 1))); + bodyRefs = Arrays.copyOf(bodyRefs, nextCapacity); + bodyUuids = Arrays.copyOf(bodyUuids, nextCapacity); + spaceUuids = Arrays.copyOf(spaceUuids, nextCapacity); + bodyTypes = Arrays.copyOf(bodyTypes, nextCapacity); + values = Arrays.copyOf(values, nextCapacity * CompactSnapshot.FLOAT_STRIDE); + } + + @SuppressWarnings("unchecked") + @Nonnull + private static Ref[] trimRefs(@Nonnull Ref[] refs, int size) { + return Arrays.copyOf(refs, size); + } + } + + public interface BodyCursor { + + @Nullable + Ref bodyRef(); + + @Nonnull + UUID bodyUuid(); + + @Nonnull + UUID spaceUuid(); + + @Nonnull + PhysicsBodyType bodyType(); + + float positionX(); + + float positionY(); + + float positionZ(); + + float rotationX(); + + float rotationY(); + + float rotationZ(); + + float rotationW(); + + float linearVelocityX(); + + float linearVelocityY(); + + float linearVelocityZ(); + + float angularVelocityX(); + + float angularVelocityY(); + + float angularVelocityZ(); + + float centerOfMassOffsetY(); + + boolean sleeping(); + } + + private static final class PublishedBodyCursor implements BodyCursor { + + @Nonnull + private final CompactSnapshot snapshot; + private int index; + + private PublishedBodyCursor(@Nonnull CompactSnapshot snapshot) { + this.snapshot = snapshot; + } + + @Nullable + @Override + public Ref bodyRef() { + return snapshot.bodyRefs[index]; + } + + @Nonnull + @Override + public UUID bodyUuid() { + return snapshot.bodyUuids[index]; + } + + @Nonnull + @Override + public UUID spaceUuid() { + return snapshot.spaceUuids[index]; + } + + @Nonnull + @Override + public PhysicsBodyType bodyType() { + return snapshot.bodyTypes[index]; + } + + @Override + public float positionX() { + return snapshot.value(index, CompactSnapshot.POSITION_X); + } + + @Override + public float positionY() { + return snapshot.value(index, CompactSnapshot.POSITION_Y); + } + + @Override + public float positionZ() { + return snapshot.value(index, CompactSnapshot.POSITION_Z); + } + + @Override + public float rotationX() { + return snapshot.value(index, CompactSnapshot.ROTATION_X); + } + + @Override + public float rotationY() { + return snapshot.value(index, CompactSnapshot.ROTATION_Y); + } + + @Override + public float rotationZ() { + return snapshot.value(index, CompactSnapshot.ROTATION_Z); + } + + @Override + public float rotationW() { + return snapshot.value(index, CompactSnapshot.ROTATION_W); + } + + @Override + public float linearVelocityX() { + return snapshot.value(index, CompactSnapshot.LINEAR_VELOCITY_X); + } + + @Override + public float linearVelocityY() { + return snapshot.value(index, CompactSnapshot.LINEAR_VELOCITY_Y); + } + + @Override + public float linearVelocityZ() { + return snapshot.value(index, CompactSnapshot.LINEAR_VELOCITY_Z); + } + + @Override + public float angularVelocityX() { + return snapshot.value(index, CompactSnapshot.ANGULAR_VELOCITY_X); + } + + @Override + public float angularVelocityY() { + return snapshot.value(index, CompactSnapshot.ANGULAR_VELOCITY_Y); + } + + @Override + public float angularVelocityZ() { + return snapshot.value(index, CompactSnapshot.ANGULAR_VELOCITY_Z); + } + + @Override + public float centerOfMassOffsetY() { + return snapshot.value(index, CompactSnapshot.CENTER_OF_MASS_OFFSET_Y); + } + + @Override + public boolean sleeping() { + return snapshot.sleeping.get(index); + } + } + + private static boolean sameRef(@Nullable Ref first, + @Nonnull Ref second) { + return first != null + && first.getIndex() == second.getIndex() + && first.getStore() == second.getStore(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSpaceBinding.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSpaceBinding.java deleted file mode 100644 index af8aedbb..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSpaceBinding.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Runtime binding between a stable core space id and backend-local execution handles. - */ -public record PhysicsSpaceBinding(@Nonnull BackendId backendId, - @Nonnull SpaceId spaceId, - @Nonnull BackendSpaceHandle backendSpaceHandle, - @Nonnull PhysicsBackendRuntime runtime) { - - public PhysicsSpaceBinding { - Objects.requireNonNull(backendId, "backendId"); - Objects.requireNonNull(spaceId, "spaceId"); - Objects.requireNonNull(backendSpaceHandle, "backendSpaceHandle"); - Objects.requireNonNull(runtime, "runtime"); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSpaceCompatibilityIndexResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSpaceCompatibilityIndexResource.java new file mode 100644 index 00000000..71d69dcb --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSpaceCompatibilityIndexResource.java @@ -0,0 +1,97 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.SpaceId; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Runtime-only compatibility map between public SpaceId values and PhysicsStore space UUIDs. + */ +public final class PhysicsSpaceCompatibilityIndexResource implements Resource { + + @Nonnull + private final Int2ObjectOpenHashMap spaceUuidsByCompatId = + new Int2ObjectOpenHashMap<>(); + @Nonnull + private final Object2IntOpenHashMap compatIdsBySpaceUuid = + new Object2IntOpenHashMap<>(); + + public PhysicsSpaceCompatibilityIndexResource() { + compatIdsBySpaceUuid.defaultReturnValue(Integer.MIN_VALUE); + } + + public void putSpace(@Nonnull SpaceId spaceId, @Nonnull UUID spaceUuid) { + int compatibilityId = spaceId.value(); + UUID previousUuid = spaceUuidsByCompatId.put(compatibilityId, + Objects.requireNonNull(spaceUuid, "spaceUuid")); + if (previousUuid != null && !previousUuid.equals(spaceUuid)) { + compatIdsBySpaceUuid.removeInt(previousUuid); + } + int previousId = compatIdsBySpaceUuid.put(spaceUuid, compatibilityId); + if (previousId != Integer.MIN_VALUE && previousId != compatibilityId) { + spaceUuidsByCompatId.remove(previousId); + } + } + + public boolean hasSpace(@Nonnull SpaceId spaceId) { + return spaceUuidsByCompatId.containsKey(spaceId.value()); + } + + @Nullable + public UUID getSpaceUuid(@Nonnull SpaceId spaceId) { + return spaceUuidsByCompatId.get(spaceId.value()); + } + + @Nullable + public SpaceId getSpaceId(@Nonnull UUID spaceUuid) { + int value = compatIdsBySpaceUuid.getInt(spaceUuid); + return value != Integer.MIN_VALUE ? new SpaceId(value) : null; + } + + public void removeBySpaceUuid(@Nonnull UUID spaceUuid) { + int value = compatIdsBySpaceUuid.removeInt(spaceUuid); + if (value != Integer.MIN_VALUE) { + spaceUuidsByCompatId.remove(value); + } + } + + @Nonnull + public Collection spaceIds() { + ArrayList ids = new ArrayList<>(spaceUuidsByCompatId.size()); + spaceUuidsByCompatId.keySet().forEach((int value) -> ids.add(new SpaceId(value))); + return ids; + } + + public int size() { + return spaceUuidsByCompatId.size(); + } + + public void clear() { + spaceUuidsByCompatId.clear(); + compatIdsBySpaceUuid.clear(); + } + + @Nonnull + @Override + public PhysicsSpaceCompatibilityIndexResource clone() { + PhysicsSpaceCompatibilityIndexResource copy = new PhysicsSpaceCompatibilityIndexResource(); + copy.spaceUuidsByCompatId.putAll(spaceUuidsByCompatId); + copy.compatIdsBySpaceUuid.putAll(compatIdsBySpaceUuid); + return copy; + } + + @Nonnull + public static ResourceType + getResourceType() { + return PhysicsResourceTypes.spaceCompatibilityIndexResourceType(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSpaceRuntime.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSpaceRuntime.java deleted file mode 100644 index 2a5738e9..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSpaceRuntime.java +++ /dev/null @@ -1,286 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources; - -import com.hypixel.hytale.logger.HytaleLogger; -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.capability.PhysicsActivationTuning; -import dev.hytalemodding.impulse.api.capability.PhysicsCapabilityId; -import dev.hytalemodding.impulse.api.capability.PhysicsSolverTuning; -import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsBackendExtensionId; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.logging.Level; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3f; - -/** - * Id-only space topology and per-space settings for one physics world. - */ -public final class PhysicsSpaceRuntime { - - private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); - - private final Int2ObjectMap spaces = new Int2ObjectOpenHashMap<>(); - - /** - * Per-space settings (world collision mode, radius, TTL, etc.). Keyed by space id value. - */ - private final Int2ObjectMap spaceSettings = - new Int2ObjectOpenHashMap<>(); - - @Nonnull - public synchronized PhysicsSpaceBinding createSpace(@Nonnull BackendId backendId, - @Nonnull SpaceId spaceId, - @Nonnull String worldName, - @Nonnull PhysicsSpaceSettings settings, - @Nonnull PhysicsStepMode stepMode) { - if (spaces.containsKey(spaceId.value())) { - throw new IllegalArgumentException("Physics space id=" + spaceId + " is already registered"); - } - SpaceId.reserveAtLeast(spaceId.value()); - - LOGGER.at(Level.FINE).log( - "World %s creating physics space using backend %s collision=%s", - worldName, - backendId, - settings.getWorldCollisionSettings().getWorldCollisionMode()); - - PhysicsBackendRuntime runtime = Impulse.createRuntime(backendId); - BackendSpaceHandle backendSpaceHandle = new BackendSpaceHandle(runtime.createSpace(spaceId)); - PhysicsSpaceBinding binding = - new PhysicsSpaceBinding(backendId, spaceId, backendSpaceHandle, runtime); - try { - validateSpaceCompatibleWithStepMode(binding, stepMode); - applySolverTuning(binding, settings); - } catch (RuntimeException exception) { - closeBindingSilently(binding, worldName, "discarding failed physics space"); - throw exception; - } - spaces.put(spaceId.value(), binding); - spaceSettings.put(spaceId.value(), new PhysicsSpaceSettings(settings)); - - LOGGER.at(Level.FINE).log( - "World %s created physics space id=%s backend=%s collision=%s", - worldName, - spaceId, - backendId, - settings.getWorldCollisionSettings().getWorldCollisionMode()); - return binding; - } - - @Nullable - public synchronized PhysicsSpaceBinding getBinding(@Nonnull SpaceId spaceId) { - return spaces.get(spaceId.value()); - } - - @Nonnull - public PhysicsSpaceBinding requireBinding(@Nonnull SpaceId spaceId) { - PhysicsSpaceBinding binding = getBinding(spaceId); - if (binding == null) { - throw new IllegalArgumentException("Physics space id=" + spaceId + " is not registered"); - } - return binding; - } - - public synchronized int getSpaceCount() { - return spaces.size(); - } - - @Nonnull - public synchronized Collection getBindings() { - return new ArrayList<>(spaces.values()); - } - - @Nonnull - public synchronized Iterable iterateBindings() { - return new ArrayList<>(spaces.values()); - } - - @Nonnull - public synchronized List getSpaceIds() { - List ids = new ArrayList<>(spaces.size()); - for (PhysicsSpaceBinding binding : spaces.values()) { - ids.add(binding.spaceId()); - } - return ids; - } - - @Nullable - public synchronized PhysicsSpaceBinding removeSpace(@Nonnull SpaceId spaceId) { - PhysicsSpaceBinding removed = spaces.remove(spaceId.value()); - spaceSettings.remove(spaceId.value()); - return removed; - } - - @Nonnull - public synchronized PhysicsRuntimeResetResult resetKeepingSpaces( - @Nonnull String worldName, - @Nonnull PhysicsStepMode stepMode) { - List previousBindings = new ArrayList<>(spaces.values()); - List replacements = new ArrayList<>(previousBindings.size()); - for (PhysicsSpaceBinding previous : previousBindings) { - PhysicsSpaceBinding replacement = null; - Vector3f gravity = new Vector3f(); - try { - PhysicsSpaceSettings settings = spaceSettings.get(previous.spaceId().value()); - if (settings == null) { - throw new IllegalStateException("Physics space settings are missing for id=" + previous.spaceId()); - } - previous.runtime().getGravity(previous.backendSpaceHandle().value(), gravity::set); - PhysicsBackendRuntime runtime = Impulse.createRuntime(previous.backendId()); - BackendSpaceHandle backendSpaceHandle = - new BackendSpaceHandle(runtime.createSpace(previous.spaceId())); - replacement = new PhysicsSpaceBinding(previous.backendId(), - previous.spaceId(), - backendSpaceHandle, - runtime); - validateSpaceCompatibleWithStepMode(replacement, stepMode); - replacement.runtime().setGravity(backendSpaceHandle.value(), gravity.x, gravity.y, gravity.z); - applySolverTuning(replacement, settings); - replacements.add(replacement); - } catch (RuntimeException exception) { - if (replacement != null) { - closeBindingSilently(replacement, worldName, "discarding failed clean replacement"); - } - closeBindingsSilently(replacements, worldName, "discarding clean replacements"); - throw exception; - } - } - - int removedBodies = 0; - int removedJoints = 0; - for (PhysicsSpaceBinding previous : previousBindings) { - removedBodies += previous.runtime().bodyCount(previous.backendSpaceHandle().value()); - removedJoints += previous.runtime().jointCount(previous.backendSpaceHandle().value()); - } - spaces.clear(); - for (PhysicsSpaceBinding replacement : replacements) { - spaces.put(replacement.spaceId().value(), replacement); - } - for (PhysicsSpaceBinding previous : previousBindings) { - closeBindingSilently(previous, worldName, "cleaned physics space"); - } - return new PhysicsRuntimeResetResult(removedBodies, removedJoints, replacements.size()); - } - - private static void closeBindingsSilently(@Nonnull Iterable bindings, - @Nonnull String worldName, - @Nonnull String reason) { - for (PhysicsSpaceBinding binding : bindings) { - closeBindingSilently(binding, worldName, reason); - } - } - - @Nonnull - public synchronized PhysicsSpaceSettings getSpaceSettings(@Nonnull SpaceId spaceId) { - return new PhysicsSpaceSettings(getLiveSpaceSettings(spaceId)); - } - - @Nonnull - public synchronized PhysicsSpaceSettings getLiveSpaceSettings(@Nonnull SpaceId spaceId) { - PhysicsSpaceSettings settings = spaceSettings.get(spaceId.value()); - if (settings == null) { - throw new IllegalStateException("Physics space settings are missing for id=" + spaceId); - } - return settings; - } - - public synchronized void setSpaceSettings(@Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceSettings settings) { - PhysicsSpaceBinding binding = spaces.get(spaceId.value()); - if (binding == null) { - throw new IllegalArgumentException("Physics space id=" + spaceId - + " is not registered"); - } - applySolverTuning(binding, settings); - spaceSettings.put(spaceId.value(), new PhysicsSpaceSettings(settings)); - } - - public synchronized void validateStepModeSupported(@Nonnull PhysicsStepMode stepMode) { - if (stepMode != PhysicsStepMode.CCD) { - return; - } - - List unsupportedSpaces = new ArrayList<>(); - for (PhysicsSpaceBinding binding : spaces.values()) { - if (!supportsContinuousCollision(binding)) { - unsupportedSpaces.add(formatSpace(binding)); - } - } - if (!unsupportedSpaces.isEmpty()) { - throw new IllegalArgumentException("CCD mode is not available for: " - + String.join(", ", unsupportedSpaces)); - } - } - - public synchronized void clearLiveTopology(@Nonnull String worldName) { - for (PhysicsSpaceBinding binding : new ArrayList<>(spaces.values())) { - closeBindingSilently(binding, worldName, "discarded copied physics space"); - } - spaces.clear(); - spaceSettings.clear(); - } - - private static void validateSpaceCompatibleWithStepMode(@Nonnull PhysicsSpaceBinding binding, - @Nonnull PhysicsStepMode stepMode) { - if (stepMode == PhysicsStepMode.CCD && !supportsContinuousCollision(binding)) { - throw new IllegalArgumentException("CCD mode is not available for " - + formatSpace(binding)); - } - } - - @Nonnull - private static String formatSpace(@Nonnull PhysicsSpaceBinding binding) { - return "space " + binding.spaceId().value() + " (" + binding.backendId().value() + ")"; - } - - private static void applySolverTuning(@Nonnull PhysicsSpaceBinding binding, - @Nonnull PhysicsSpaceSettings settings) { - if (binding.runtime().supportsSolverTuning(binding.backendSpaceHandle().value())) { - binding.runtime().applySolverTuning(binding.backendSpaceHandle().value(), - new PhysicsSolverTuning( - settings.getSolverSettings().getSolverIterations(), - settings.getSolverSettings().getStabilizationIterations())); - } - if (binding.runtime().supportsActivationTuning(binding.backendSpaceHandle().value())) { - binding.runtime().applyActivationTuning(binding.backendSpaceHandle().value(), - new PhysicsActivationTuning( - settings.getSolverSettings().getDynamicSleepLinearThreshold(), - settings.getSolverSettings().getDynamicSleepAngularThreshold(), - settings.getSolverSettings().getDynamicSleepTimeUntilSleep())); - } - for (PhysicsBackendExtensionId extensionId : settings.getExtensionSettings().asMap().keySet()) { - binding.runtime().applyExtensionSettings(binding.backendSpaceHandle().value(), - new PhysicsCapabilityId(extensionId.value()), - consumer -> settings.getExtensionSettings().asStringMap(extensionId).forEach(consumer)); - } - } - - private static boolean supportsContinuousCollision(@Nonnull PhysicsSpaceBinding binding) { - return binding.runtime().supportsContinuousCollision(binding.backendSpaceHandle().value()); - } - - static void closeBindingSilently(@Nonnull PhysicsSpaceBinding binding, - @Nonnull String worldName, - @Nonnull String action) { - try { - binding.runtime().destroySpace(binding.backendSpaceHandle().value()); - } catch (RuntimeException exception) { - LOGGER.at(Level.WARNING).log( - "World %s failed to close %s id=%s backend=%s: %s", - worldName, - action, - binding.spaceId(), - binding.backendId(), - exception.getMessage()); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStepSchedulerResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStepSchedulerResource.java new file mode 100644 index 00000000..08eee14d --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStepSchedulerResource.java @@ -0,0 +1,388 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsStepPhaseStats; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsFrameEvent; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepSchedulingMode; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Per-PhysicsStore backend owner lane. + * + *

Only the backend step itself runs off the world thread. PhysicsStore ECS systems are skipped + * while a step is pending, so backend mutations and snapshot reads remain serialized around the + * owner lane.

+ */ +public final class PhysicsStepSchedulerResource implements Resource, AutoCloseable { + + private static final AtomicInteger THREAD_COUNTER = new AtomicInteger(); + + @Nonnull + private final ExecutorService ownerLane = Executors.newSingleThreadExecutor( + new OwnerLaneThreadFactory()); + @Nullable + private PendingStep pendingStep; + @Nullable + private CompletedStep completedStep; + private float backlogDtSeconds; + private float lastSubmittedDtSeconds; + private boolean droppedPendingDtSinceLastStep; + private boolean closed; + + public PhysicsStepSchedulerResource() { + } + + @Nonnull + public synchronized TickDecision beforeStoreTick(float dtSeconds, + @Nonnull PhysicsStepSchedulingMode mode, + float maxSubmittedDtSeconds, + long nowNanos) { + pollPendingStep(); + if (pendingStep == null) { + return TickDecision.tick(); + } + StepInput skipped = accumulatePendingDt(dtSeconds, mode, maxSubmittedDtSeconds); + return TickDecision.skip(pendingAgeNanos(nowNanos), skipped); + } + + @Nonnull + public synchronized StepInput acceptStepInput(float dtSeconds, + @Nonnull PhysicsStepSchedulingMode mode, + float maxSubmittedDtSeconds) { + pollPendingStep(); + if (pendingStep != null) { + throw new IllegalStateException("Cannot submit a PhysicsStore step while another step is pending"); + } + float inputDtSeconds = safeDt(dtSeconds); + float candidateDtSeconds = inputDtSeconds; + float droppedDtSeconds = 0.0f; + if (mode == PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT) { + candidateDtSeconds += backlogDtSeconds; + } else { + // Dropped pending ticks may reappear as one large engine dt after the owner lane clears. + if (droppedPendingDtSinceLastStep && lastSubmittedDtSeconds > 0.0f) { + float steadyDtSeconds = Math.min(candidateDtSeconds, lastSubmittedDtSeconds); + droppedDtSeconds = candidateDtSeconds - steadyDtSeconds; + candidateDtSeconds = steadyDtSeconds; + } + backlogDtSeconds = 0.0f; + } + SubmittedDt submittedDt = capSubmittedDt(candidateDtSeconds, maxSubmittedDtSeconds); + if (submittedDt.submittedDtSeconds() > 0.0f) { + lastSubmittedDtSeconds = submittedDt.submittedDtSeconds(); + } + backlogDtSeconds = 0.0f; + droppedPendingDtSinceLastStep = false; + return new StepInput(inputDtSeconds, + submittedDt.submittedDtSeconds(), + backlogDtSeconds, + droppedDtSeconds + submittedDt.droppedDtSeconds(), + submittedDt.dtCapHit()); + } + + public boolean submitStep(@Nonnull StepInput input, + @Nonnull StepTask task, + long nowNanos) { + Objects.requireNonNull(input, "input"); + Objects.requireNonNull(task, "task"); + synchronized (this) { + pollPendingStep(); + if (closed || pendingStep != null) { + return false; + } + CompletableFuture future = CompletableFuture.supplyAsync( + () -> task.run().withInput(input), + ownerLane); + pendingStep = new PendingStep(input, future, Math.max(0L, nowNanos)); + return true; + } + } + + public synchronized boolean isStepPending() { + pollPendingStep(); + return pendingStep != null; + } + + @Nonnull + public synchronized CompletionStage whenIdle() { + pollPendingStep(); + PendingStep pending = pendingStep; + if (pending == null) { + return CompletableFuture.completedFuture(null).minimalCompletionStage(); + } + return pending.future().thenApply(_ -> (Void) null).minimalCompletionStage(); + } + + @Nullable + public synchronized CompletedStep pollCompletedStep() { + pollPendingStep(); + CompletedStep completed = completedStep; + completedStep = null; + return completed; + } + + @Override + public void close() { + PendingStep pending; + synchronized (this) { + closed = true; + pending = pendingStep; + } + if (pending != null) { + try { + pending.future().join(); + } catch (CompletionException ignored) { + // The next store tick will publish the failure if shutdown did not consume it first. + } + } + ownerLane.shutdown(); + } + + @Nonnull + @Override + public PhysicsStepSchedulerResource clone() { + return new PhysicsStepSchedulerResource(); + } + + @Nonnull + public static ResourceType getResourceType() { + return PhysicsResourceTypes.stepSchedulerResourceType(); + } + + private StepInput accumulatePendingDt(float dtSeconds, + @Nonnull PhysicsStepSchedulingMode mode, + float maxSubmittedDtSeconds) { + float inputDtSeconds = safeDt(dtSeconds); + if (inputDtSeconds <= 0.0f) { + return new StepInput(0.0f, 0.0f, backlogDtSeconds, 0.0f, false); + } + if (mode != PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT) { + droppedPendingDtSinceLastStep = true; + return new StepInput(inputDtSeconds, 0.0f, backlogDtSeconds, inputDtSeconds, false); + } + droppedPendingDtSinceLastStep = false; + SubmittedDt submittedDt = capSubmittedDt(backlogDtSeconds + inputDtSeconds, + maxSubmittedDtSeconds); + backlogDtSeconds = submittedDt.submittedDtSeconds(); + return new StepInput(inputDtSeconds, + 0.0f, + backlogDtSeconds, + submittedDt.droppedDtSeconds(), + submittedDt.dtCapHit()); + } + + private void pollPendingStep() { + PendingStep pending = pendingStep; + if (pending == null || !pending.future().isDone()) { + return; + } + try { + completedStep = pending.future().join(); + } catch (CompletionException exception) { + completedStep = CompletedStep.failed(pending.input(), + exception.getCause() != null ? exception.getCause() : exception); + } + pendingStep = null; + } + + private long pendingAgeNanos(long nowNanos) { + PendingStep pending = pendingStep; + return pending == null ? 0L : Math.max(0L, nowNanos - pending.startedNanos()); + } + + private static float safeDt(float dtSeconds) { + return Float.isFinite(dtSeconds) ? Math.max(0.0f, dtSeconds) : 0.0f; + } + + private static SubmittedDt capSubmittedDt(float dtSeconds, float maxSubmittedDtSeconds) { + float safeDtSeconds = safeDt(dtSeconds); + float safeMax = Float.isFinite(maxSubmittedDtSeconds) && maxSubmittedDtSeconds > 0.0f + ? maxSubmittedDtSeconds + : safeDtSeconds; + if (safeDtSeconds <= safeMax) { + return new SubmittedDt(safeDtSeconds, 0.0f, false); + } + return new SubmittedDt(safeMax, safeDtSeconds - safeMax, true); + } + + @FunctionalInterface + public interface StepTask { + + @Nonnull + CompletedStep run(); + } + + public record StepInput(float inputDtSeconds, + float submittedDtSeconds, + float backlogDtSeconds, + float droppedBacklogDtSeconds, + boolean dtCapHit) { + + public StepInput { + inputDtSeconds = safeDt(inputDtSeconds); + submittedDtSeconds = safeDt(submittedDtSeconds); + backlogDtSeconds = safeDt(backlogDtSeconds); + droppedBacklogDtSeconds = safeDt(droppedBacklogDtSeconds); + } + } + + public record TickDecision(boolean shouldTick, + long pendingStepAgeNanos, + float inputDtSeconds, + float submittedDtSeconds, + float backlogDtSeconds, + float droppedBacklogDtSeconds, + boolean dtCapHit) { + + private static TickDecision tick() { + return new TickDecision(true, 0L, 0.0f, 0.0f, 0.0f, 0.0f, false); + } + + private static TickDecision skip(long pendingStepAgeNanos, @Nonnull StepInput input) { + return new TickDecision(false, + Math.max(0L, pendingStepAgeNanos), + input.inputDtSeconds(), + input.submittedDtSeconds(), + input.backlogDtSeconds(), + input.droppedBacklogDtSeconds(), + input.dtCapHit()); + } + } + + public record CompletedStep(@Nullable StepInput input, + int spaces, + int substeps, + long stepSubmitNanos, + long snapshotNanos, + @Nonnull PhysicsStepPhaseStats nativePhaseStats, + @Nonnull PhysicsSnapshotResource.CompactSnapshot bodySnapshot, + @Nonnull List physicsEvents, + int droppedBackendEventCount, + @Nullable Throwable failure) { + + public CompletedStep(int spaces, + int substeps, + long stepSubmitNanos, + @Nonnull PhysicsStepPhaseStats nativePhaseStats) { + this(spaces, + substeps, + stepSubmitNanos, + 0L, + nativePhaseStats, + PhysicsSnapshotResource.emptyCompactSnapshot()); + } + + public CompletedStep(int spaces, + int substeps, + long stepSubmitNanos, + long snapshotNanos, + @Nonnull PhysicsStepPhaseStats nativePhaseStats, + @Nonnull PhysicsSnapshotResource.CompactSnapshot bodySnapshot) { + this(spaces, + substeps, + stepSubmitNanos, + snapshotNanos, + nativePhaseStats, + bodySnapshot, + List.of(), + 0); + } + + public CompletedStep(int spaces, + int substeps, + long stepSubmitNanos, + long snapshotNanos, + @Nonnull PhysicsStepPhaseStats nativePhaseStats, + @Nonnull PhysicsSnapshotResource.CompactSnapshot bodySnapshot, + @Nonnull List physicsEvents, + int droppedBackendEventCount) { + this(null, + spaces, + substeps, + stepSubmitNanos, + snapshotNanos, + nativePhaseStats, + bodySnapshot, + physicsEvents, + droppedBackendEventCount, + null); + } + + public CompletedStep { + spaces = Math.max(0, spaces); + substeps = Math.max(0, substeps); + stepSubmitNanos = Math.max(0L, stepSubmitNanos); + snapshotNanos = Math.max(0L, snapshotNanos); + Objects.requireNonNull(nativePhaseStats, "nativePhaseStats"); + Objects.requireNonNull(bodySnapshot, "bodySnapshot"); + physicsEvents = List.copyOf(Objects.requireNonNull(physicsEvents, + "physicsEvents")); + droppedBackendEventCount = Math.max(0, droppedBackendEventCount); + } + + @Nonnull + private CompletedStep withInput(@Nonnull StepInput input) { + return new CompletedStep(input, + spaces, + substeps, + stepSubmitNanos, + snapshotNanos, + nativePhaseStats, + bodySnapshot, + physicsEvents, + droppedBackendEventCount, + failure); + } + + private static CompletedStep failed(@Nonnull StepInput input, + @Nonnull Throwable failure) { + return new CompletedStep(input, + 0, + 0, + 0L, + 0L, + PhysicsStepPhaseStats.unavailable(), + PhysicsSnapshotResource.emptyCompactSnapshot(), + List.of(), + 0, + Objects.requireNonNull(failure, "failure")); + } + + public boolean failed() { + return failure != null; + } + } + + private record PendingStep(@Nonnull StepInput input, + @Nonnull CompletableFuture future, + long startedNanos) { + } + + private record SubmittedDt(float submittedDtSeconds, + float droppedDtSeconds, + boolean dtCapHit) { + } + + private static final class OwnerLaneThreadFactory implements ThreadFactory { + + @Override + public Thread newThread(@Nonnull Runnable runnable) { + Thread thread = new Thread(runnable, + "Impulse PhysicsStore owner lane-" + THREAD_COUNTER.incrementAndGet()); + thread.setDaemon(true); + return thread; + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStoreReadQueueResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStoreReadQueueResource.java new file mode 100644 index 00000000..7a90090d --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStoreReadQueueResource.java @@ -0,0 +1,106 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsAsyncCompletions; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; +import javax.annotation.Nonnull; + +/** + * World-thread backend read queue drained by PhysicsStore systems. + * + *

Reads execute during PhysicsStore ticking. Callers must pass value-copied inputs and return + * immutable values rather than live stores, refs, runtime resources, or backend handles. This is a + * reviewable caller contract; do not enforce it with generic runtime object inspection in this hot + * path.

+ */ +public final class PhysicsStoreReadQueueResource implements Resource { + + @Nonnull + private final Queue> reads = new ArrayDeque<>(); + + public PhysicsStoreReadQueueResource() { + } + + @Nonnull + public synchronized CompletionStage enqueueRead( + @Nonnull Function, R> read) { + CompletableFuture completion = new CompletableFuture<>(); + reads.add(new QueuedRead<>(read, completion)); + return completion.minimalCompletionStage(); + } + + @Nonnull + public synchronized List> drain() { + List> drained = new ArrayList<>(reads.size()); + QueuedRead read; + while ((read = reads.poll()) != null) { + drained.add(read); + } + return drained; + } + + public void clear() { + List> drained; + synchronized (this) { + drained = new ArrayList<>(reads); + reads.clear(); + } + CancellationException cancelled = + new CancellationException("PhysicsStore read queue cleared"); + for (QueuedRead read : drained) { + read.fail(cancelled); + } + } + + public synchronized int size() { + return reads.size(); + } + + @Nonnull + @Override + public PhysicsStoreReadQueueResource clone() { + return new PhysicsStoreReadQueueResource(); + } + + @Nonnull + public static ResourceType getResourceType() { + return PhysicsResourceTypes.readQueueResourceType(); + } + + public static final class QueuedRead { + + @Nonnull + private final Function, R> read; + @Nonnull + private final CompletableFuture completion; + + private QueuedRead(@Nonnull Function, R> read, + @Nonnull CompletableFuture completion) { + this.read = Objects.requireNonNull(read, "read"); + this.completion = Objects.requireNonNull(completion, "completion"); + } + + public void complete(@Nonnull Store store) { + try { + PhysicsAsyncCompletions.complete(completion, read.apply(store)); + } catch (RuntimeException | Error exception) { + fail(exception); + } + } + + public void fail(@Nonnull Throwable failure) { + PhysicsAsyncCompletions.fail(completion, Objects.requireNonNull(failure, "failure")); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsVisualInterestResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsVisualInterestResource.java new file mode 100644 index 00000000..f6a40047 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsVisualInterestResource.java @@ -0,0 +1,173 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime.BodyVisualInterestState; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime.VisualInterest; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Runtime-only EntityStore visual-interest state for generated physics visuals. + */ +public final class PhysicsVisualInterestResource implements Resource { + + @Nullable + private static ResourceType resourceType; + + private final List syntheticVisualInterests = new ArrayList<>(); + private final Map bodyVisualInterestStates = + new Object2ObjectOpenHashMap<>(); + private final Int2ObjectOpenHashMap bodyVisualInterestStatesByRowIndex = + new Int2ObjectOpenHashMap<>(); + private final AtomicLong visualInterestTick = new AtomicLong(); + + public synchronized void setSyntheticVisualInterests( + @Nonnull Collection interests) { + syntheticVisualInterests.clear(); + syntheticVisualInterests.addAll(interests); + } + + @Nonnull + public synchronized List getSyntheticVisualInterests() { + return new ArrayList<>(syntheticVisualInterests); + } + + public synchronized void clearSyntheticVisualInterests() { + syntheticVisualInterests.clear(); + } + + @Nonnull + public synchronized BodyVisualInterestState getOrCreateBodyVisualInterestState( + @Nonnull UUID bodyUuid, + @Nullable Ref bodyRef) { + BodyVisualInterestState state; + if (isValidRef(bodyRef)) { + state = getOrCreateBodyVisualInterestState(bodyRef); + } else { + state = bodyVisualInterestStates.computeIfAbsent(bodyUuid, + _ -> new BodyVisualInterestState()); + } + state.advanceVisualInterestTick(visualInterestTick.get()); + return state; + } + + @Nullable + public synchronized BodyVisualInterestState getBodyVisualInterestState( + @Nonnull UUID bodyUuid, + @Nullable Ref bodyRef) { + BodyVisualInterestState state; + if (isValidRef(bodyRef)) { + int rowIndex = bodyRef.getIndex(); + BodyVisualInterestRefState row = bodyVisualInterestStatesByRowIndex.get(rowIndex); + if (row == null) { + return null; + } + if (!isMatchingLiveRef(row, bodyRef)) { + bodyVisualInterestStatesByRowIndex.remove(rowIndex); + return null; + } + state = row.state(); + } else { + state = bodyVisualInterestStates.get(bodyUuid); + } + if (state != null) { + state.advanceVisualInterestTick(visualInterestTick.get()); + } + return state; + } + + public synchronized void clearBodyVisualInterestState(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef) { + bodyVisualInterestStates.remove(bodyUuid); + if (!isValidRef(bodyRef)) { + return; + } + int rowIndex = bodyRef.getIndex(); + BodyVisualInterestRefState row = bodyVisualInterestStatesByRowIndex.get(rowIndex); + if (row != null && (!row.bodyRef().isValid() || sameRef(row.bodyRef(), bodyRef))) { + bodyVisualInterestStatesByRowIndex.remove(rowIndex); + } + } + + public long advanceVisualInterestTick() { + return visualInterestTick.incrementAndGet(); + } + + @Nonnull + @Override + public PhysicsVisualInterestResource clone() { + PhysicsVisualInterestResource copy = new PhysicsVisualInterestResource(); + synchronized (this) { + copy.syntheticVisualInterests.addAll(syntheticVisualInterests); + copy.bodyVisualInterestStates.putAll(bodyVisualInterestStates); + for (var entry : bodyVisualInterestStatesByRowIndex.int2ObjectEntrySet()) { + BodyVisualInterestRefState row = entry.getValue(); + copy.bodyVisualInterestStatesByRowIndex.put(entry.getIntKey(), + new BodyVisualInterestRefState(row.bodyRef(), row.state())); + } + copy.visualInterestTick.set(visualInterestTick.get()); + } + return copy; + } + + @Nullable + public static ResourceType getResourceType() { + return resourceType; + } + + public static void setResourceType( + @Nonnull ResourceType type) { + resourceType = type; + } + + public static void clearResourceType() { + resourceType = null; + } + + @Nonnull + private BodyVisualInterestState getOrCreateBodyVisualInterestState( + @Nonnull Ref bodyRef) { + int rowIndex = bodyRef.getIndex(); + BodyVisualInterestRefState row = bodyVisualInterestStatesByRowIndex.get(rowIndex); + if (row == null || !isMatchingLiveRef(row, bodyRef)) { + row = new BodyVisualInterestRefState(bodyRef, new BodyVisualInterestState()); + bodyVisualInterestStatesByRowIndex.put(rowIndex, row); + } + return row.state(); + } + + private static boolean isMatchingLiveRef(@Nonnull BodyVisualInterestRefState row, + @Nonnull Ref bodyRef) { + return row.bodyRef().isValid() && sameRef(row.bodyRef(), bodyRef); + } + + private static boolean isValidRef(@Nullable Ref ref) { + return ref != null && ref.isValid(); + } + + private static boolean sameRef(@Nullable Ref first, + @Nullable Ref second) { + return first == second + || (first != null + && second != null + && first.getStore() != null + && first.getStore() == second.getStore() + && first.getIndex() == second.getIndex()); + } + + private record BodyVisualInterestRefState(@Nonnull Ref bodyRef, + @Nonnull BodyVisualInterestState state) { + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsVisualRuntime.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsVisualRuntime.java index f9a5ebd0..7b26fe73 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsVisualRuntime.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsVisualRuntime.java @@ -2,8 +2,9 @@ import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.view.RaycastHitView; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.physics.RaycastHitView; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import java.util.ArrayList; @@ -13,6 +14,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.Consumer; @@ -28,42 +30,81 @@ public final class PhysicsVisualRuntime { @Nonnull private final Consumer> syncStateCleaner; - private final Map>> bodyAttachments = + private final Map>> bodyAttachments = new Object2ObjectOpenHashMap<>(); - private final Map> generatedVisualProxies = + private final Int2ObjectOpenHashMap bodyAttachmentsByRowIndex = + new Int2ObjectOpenHashMap<>(); + private final Map> generatedVisualProxies = new Object2ObjectOpenHashMap<>(); + private final Int2ObjectOpenHashMap generatedVisualProxiesByRowIndex = + new Int2ObjectOpenHashMap<>(); private final List syntheticVisualInterests = new ArrayList<>(); - private final Map bodyVisualInterestStates = + private final Map bodyVisualInterestStates = new Object2ObjectOpenHashMap<>(); + private final Int2ObjectOpenHashMap bodyVisualInterestStatesByRowIndex = + new Int2ObjectOpenHashMap<>(); public PhysicsVisualRuntime(@Nonnull Consumer> syncStateCleaner) { this.syncStateCleaner = syncStateCleaner; } - public synchronized void registerAttachment(@Nonnull RigidBodyKey bodyKey, @Nonnull Ref attachment) { - bodyAttachments.computeIfAbsent(bodyKey, _ -> new ObjectOpenHashSet<>()) + public synchronized void registerAttachment(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef, + @Nonnull Ref attachment) { + bodyAttachments.computeIfAbsent(bodyUuid, _ -> new ObjectOpenHashSet<>()) .add(attachment); + if (isValidRef(bodyRef)) { + bodyAttachmentRefs(bodyRef).add(attachment); + } } - public synchronized void unregisterAttachment(@Nonnull RigidBodyKey bodyKey, + public synchronized void unregisterAttachment(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef, @Nonnull Ref attachment) { - Set> attachments = bodyAttachments.get(bodyKey); + Set> attachments = bodyAttachments.get(bodyUuid); if (attachments == null) { + if (isValidRef(bodyRef)) { + unregisterAttachmentRef(bodyRef, attachment); + } return; } - attachments.remove(attachment); if (attachments.isEmpty()) { - bodyAttachments.remove(bodyKey); + bodyAttachments.remove(bodyUuid); + } + if (isValidRef(bodyRef)) { + unregisterAttachmentRef(bodyRef, attachment); } } @Nonnull - public Collection> getAttachments(@Nonnull RigidBodyKey bodyKey) { + public Collection> getAttachments(@Nonnull Ref bodyRef) { + if (!bodyRef.isValid()) { + return List.of(); + } + return liveAttachments(bodyRef); + } + + @Nonnull + public Collection> getAttachments(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef) { + if (isValidRef(bodyRef)) { + Collection> attachments = liveAttachments(bodyRef); + if (!attachments.isEmpty()) { + return attachments; + } + } + return liveAttachments(bodyAttachments, bodyUuid); + } + + @Nonnull + private Collection> liveAttachments( + @Nonnull Map>> attachmentsByKey, + @Nonnull K key) { List> liveAttachments = new ArrayList<>(); List> staleAttachments = new ArrayList<>(); synchronized (this) { - Set> attachments = bodyAttachments.get(bodyKey); + Set> attachments = attachmentsByKey.get(key); if (attachments == null || attachments.isEmpty()) { return List.of(); } @@ -77,18 +118,29 @@ public Collection> getAttachments(@Nonnull RigidBodyKey bodyKey } staleAttachments.forEach(attachments::remove); if (attachments.isEmpty()) { - bodyAttachments.remove(bodyKey); + attachmentsByKey.remove(key); } } cleanSyncStates(staleAttachments); return liveAttachments; } - public boolean hasAttachments(@Nonnull RigidBodyKey bodyKey) { + public boolean hasAttachments(@Nonnull Ref bodyRef) { + return bodyRef.isValid() && hasLiveAttachments(bodyRef); + } + + public boolean hasAttachments(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef) { + return isValidRef(bodyRef) && hasLiveAttachments(bodyRef) + || hasLiveAttachments(bodyAttachments, bodyUuid); + } + + private boolean hasLiveAttachments(@Nonnull Map>> attachmentsByKey, + @Nonnull K key) { boolean hasLiveAttachment = false; List> staleAttachments = new ArrayList<>(); synchronized (this) { - Set> attachments = bodyAttachments.get(bodyKey); + Set> attachments = attachmentsByKey.get(key); if (attachments == null || attachments.isEmpty()) { return false; } @@ -105,7 +157,7 @@ public boolean hasAttachments(@Nonnull RigidBodyKey bodyKey) { } } if (attachments.isEmpty()) { - bodyAttachments.remove(bodyKey); + attachmentsByKey.remove(key); } } cleanSyncStates(staleAttachments); @@ -113,13 +165,35 @@ public boolean hasAttachments(@Nonnull RigidBodyKey bodyKey) { } @Nullable - public Ref getGeneratedVisualProxy(@Nonnull RigidBodyKey bodyKey) { + public Ref getGeneratedVisualProxy(@Nonnull Ref bodyRef) { + if (!bodyRef.isValid()) { + return null; + } + return liveGeneratedVisualProxy(bodyRef); + } + + @Nullable + public Ref getGeneratedVisualProxy(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef) { + if (isValidRef(bodyRef)) { + Ref proxy = liveGeneratedVisualProxy(bodyRef); + if (proxy != null) { + return proxy; + } + } + return liveGeneratedVisualProxy(generatedVisualProxies, bodyUuid); + } + + @Nullable + private Ref liveGeneratedVisualProxy( + @Nonnull Map> proxiesByKey, + @Nonnull K key) { Ref staleProxy = null; Ref proxy; synchronized (this) { - proxy = generatedVisualProxies.get(bodyKey); + proxy = proxiesByKey.get(key); if (proxy != null && !proxy.isValid()) { - generatedVisualProxies.remove(bodyKey); + proxiesByKey.remove(key); staleProxy = proxy; proxy = null; } @@ -128,39 +202,14 @@ public Ref getGeneratedVisualProxy(@Nonnull RigidBodyKey bodyKey) { return proxy; } - @Nonnull - public Collection getGeneratedVisualProxyBodyKeys() { - List bodyKeys = new ArrayList<>(); - List staleBodyKeys = new ArrayList<>(); - List> staleProxies = new ArrayList<>(); - synchronized (this) { - for (Map.Entry> entry : generatedVisualProxies.entrySet()) { - Ref proxy = entry.getValue(); - if (proxy != null && proxy.isValid()) { - bodyKeys.add(entry.getKey()); - } else { - staleBodyKeys.add(entry.getKey()); - if (proxy != null) { - staleProxies.add(proxy); - } - } - } - for (RigidBodyKey bodyKey : staleBodyKeys) { - generatedVisualProxies.remove(bodyKey); - } - } - cleanSyncStates(staleProxies); - return bodyKeys; - } - public int generatedVisualProxyCount() { int count = 0; List> staleProxies = new ArrayList<>(); synchronized (this) { - Iterator>> iterator = + Iterator>> iterator = generatedVisualProxies.entrySet().iterator(); while (iterator.hasNext()) { - Map.Entry> entry = iterator.next(); + Map.Entry> entry = iterator.next(); Ref proxy = entry.getValue(); if (proxy != null && proxy.isValid()) { count++; @@ -176,46 +225,94 @@ public int generatedVisualProxyCount() { return count; } - public void setGeneratedVisualProxy(@Nonnull RigidBodyKey bodyKey, + public void setGeneratedVisualProxy(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef, @Nonnull Ref proxy) { Ref previousProxy; + Ref previousRefProxy = null; synchronized (this) { - previousProxy = generatedVisualProxies.put(bodyKey, proxy); + previousProxy = generatedVisualProxies.put(bodyUuid, proxy); + if (isValidRef(bodyRef)) { + GeneratedVisualProxyRef previous = generatedVisualProxiesByRowIndex.put(bodyRef.getIndex(), + new GeneratedVisualProxyRef(bodyUuid, bodyRef, proxy)); + if (previous != null) { + previousRefProxy = previous.proxy(); + } + } } - if (previousProxy != proxy) { + if (!sameRef(previousProxy, proxy)) { cleanSyncState(previousProxy); } + if (!sameRef(previousRefProxy, proxy) && !sameRef(previousRefProxy, previousProxy)) { + cleanSyncState(previousRefProxy); + } } - public void clearGeneratedVisualProxy(@Nonnull RigidBodyKey bodyKey) { + public void clearGeneratedVisualProxy(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef) { Ref proxy; + Ref refProxy = null; synchronized (this) { - proxy = generatedVisualProxies.remove(bodyKey); + proxy = generatedVisualProxies.remove(bodyUuid); + if (isValidRef(bodyRef)) { + GeneratedVisualProxyRef removed = removeGeneratedVisualProxyRef(bodyRef); + if (removed != null) { + refProxy = removed.proxy(); + } + } } cleanSyncState(proxy); + if (!sameRef(refProxy, proxy)) { + cleanSyncState(refProxy); + } } - public boolean clearGeneratedVisualProxy(@Nonnull RigidBodyKey bodyKey, + public boolean clearGeneratedVisualProxy(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef, @Nonnull Ref expectedProxy) { - Ref proxy; + Ref proxy = null; + Ref refProxy = null; + boolean matched = false; synchronized (this) { - proxy = generatedVisualProxies.get(bodyKey); - if (proxy == null || !sameRef(proxy, expectedProxy)) { + Ref uuidProxy = generatedVisualProxies.get(bodyUuid); + if (sameRef(uuidProxy, expectedProxy)) { + generatedVisualProxies.remove(bodyUuid); + proxy = uuidProxy; + matched = true; + } + if (isValidRef(bodyRef)) { + GeneratedVisualProxyRef removed = removeGeneratedVisualProxyRef(bodyRef, expectedProxy); + if (removed != null) { + refProxy = removed.proxy(); + matched = true; + } + } + if (!matched) { return false; } - - generatedVisualProxies.remove(bodyKey); } cleanSyncState(proxy); + if (!sameRef(refProxy, proxy)) { + cleanSyncState(refProxy); + } return true; } - public synchronized boolean isGeneratedVisualProxy(@Nonnull RigidBodyKey bodyKey, + public synchronized boolean isGeneratedVisualProxy(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef, @Nonnull Ref proxy) { - Ref registeredProxy = generatedVisualProxies.get(bodyKey); + if (isValidRef(bodyRef) && sameRef(liveGeneratedVisualProxy(bodyRef), proxy)) { + return true; + } + Ref registeredProxy = generatedVisualProxies.get(bodyUuid); return registeredProxy != null && sameRef(registeredProxy, proxy); } + public synchronized boolean isGeneratedVisualProxy(@Nonnull Ref bodyRef, + @Nonnull Ref proxy) { + return bodyRef.isValid() && sameRef(liveGeneratedVisualProxy(bodyRef), proxy); + } + public synchronized void setSyntheticVisualInterests( @Nonnull Collection interests) { syntheticVisualInterests.clear(); @@ -233,37 +330,87 @@ public synchronized void clearSyntheticVisualInterests() { @Nonnull public synchronized BodyVisualInterestState getOrCreateBodyVisualInterestState( - @Nonnull RigidBodyKey bodyKey) { - return bodyVisualInterestStates.computeIfAbsent(bodyKey, + @Nonnull UUID bodyUuid, + @Nullable Ref bodyRef) { + if (bodyRef != null && bodyRef.isValid()) { + return getOrCreateBodyVisualInterestState(bodyRef); + } + return bodyVisualInterestStates.computeIfAbsent(bodyUuid, _ -> new BodyVisualInterestState()); } @Nullable public synchronized BodyVisualInterestState getBodyVisualInterestState( - @Nonnull RigidBodyKey bodyKey) { - return bodyVisualInterestStates.get(bodyKey); + @Nonnull UUID bodyUuid, + @Nullable Ref bodyRef) { + if (bodyRef != null && bodyRef.isValid()) { + int rowIndex = bodyRef.getIndex(); + BodyVisualInterestRefState row = bodyVisualInterestStatesByRowIndex.get(rowIndex); + if (row == null) { + return null; + } + if (!isMatchingLiveRef(row, bodyRef)) { + bodyVisualInterestStatesByRowIndex.remove(rowIndex); + return null; + } + return row.state(); + } + return bodyVisualInterestStates.get(bodyUuid); + } + + public synchronized void clearBodyVisualInterestState(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef) { + bodyVisualInterestStates.remove(bodyUuid); + if (!isValidRef(bodyRef)) { + return; + } + int rowIndex = bodyRef.getIndex(); + BodyVisualInterestRefState row = bodyVisualInterestStatesByRowIndex.get(rowIndex); + if (row != null && (!row.bodyRef().isValid() || sameRef(row.bodyRef(), bodyRef))) { + bodyVisualInterestStatesByRowIndex.remove(rowIndex); + } } - public void clearBodyRuntimeState(@Nonnull RigidBodyKey bodyKey) { + public void clearBodyRuntimeState(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef) { List> staleRefs = new ArrayList<>(); synchronized (this) { - Set> attachments = bodyAttachments.remove(bodyKey); + Set> attachments = bodyAttachments.remove(bodyUuid); if (attachments != null) { staleRefs.addAll(attachments); } - Ref proxy = generatedVisualProxies.remove(bodyKey); + if (isValidRef(bodyRef)) { + BodyAttachmentRefs attachmentRefs = removeAttachmentRefs(bodyRef); + if (attachmentRefs != null) { + staleRefs.addAll(attachmentRefs.attachments()); + } + } + Ref proxy = generatedVisualProxies.remove(bodyUuid); if (proxy != null) { staleRefs.add(proxy); } - bodyVisualInterestStates.remove(bodyKey); + if (isValidRef(bodyRef)) { + GeneratedVisualProxyRef proxyRef = removeGeneratedVisualProxyRef(bodyRef); + if (proxyRef != null && !sameRef(proxyRef.proxy(), proxy)) { + staleRefs.add(proxyRef.proxy()); + } + } + bodyVisualInterestStates.remove(bodyUuid); + if (isValidRef(bodyRef)) { + int rowIndex = bodyRef.getIndex(); + BodyVisualInterestRefState row = bodyVisualInterestStatesByRowIndex.get(rowIndex); + if (row != null && (!row.bodyRef().isValid() || sameRef(row.bodyRef(), bodyRef))) { + bodyVisualInterestStatesByRowIndex.remove(rowIndex); + } + } } cleanSyncStates(staleRefs); } public void clear() { - List> staleRefs = new ArrayList<>(); + List> staleRefs; synchronized (this) { - staleRefs.addAll(generatedVisualProxies.values()); + staleRefs = new ArrayList<>(generatedVisualProxies.values()); for (Set> attachments : bodyAttachments.values()) { for (Ref attachment : attachments) { if (attachment != null) { @@ -271,14 +418,229 @@ public void clear() { } } } + for (var entry : bodyAttachmentsByRowIndex.int2ObjectEntrySet()) { + staleRefs.addAll(entry.getValue().attachments()); + } + for (var entry : generatedVisualProxiesByRowIndex.int2ObjectEntrySet()) { + Ref proxy = entry.getValue().proxy(); + staleRefs.add(proxy); + } bodyAttachments.clear(); + bodyAttachmentsByRowIndex.clear(); generatedVisualProxies.clear(); + generatedVisualProxiesByRowIndex.clear(); syntheticVisualInterests.clear(); bodyVisualInterestStates.clear(); + bodyVisualInterestStatesByRowIndex.clear(); } cleanSyncStates(staleRefs); } + @Nonnull + private BodyVisualInterestState getOrCreateBodyVisualInterestState( + @Nonnull Ref bodyRef) { + int rowIndex = bodyRef.getIndex(); + BodyVisualInterestRefState row = bodyVisualInterestStatesByRowIndex.get(rowIndex); + if (row == null || !isMatchingLiveRef(row, bodyRef)) { + row = new BodyVisualInterestRefState(bodyRef, new BodyVisualInterestState()); + bodyVisualInterestStatesByRowIndex.put(rowIndex, row); + } + return row.state(); + } + + private static boolean isMatchingLiveRef(@Nonnull BodyVisualInterestRefState row, + @Nonnull Ref bodyRef) { + return row.bodyRef().isValid() && sameRef(row.bodyRef(), bodyRef); + } + + private static boolean isMatchingLiveRef(@Nonnull BodyAttachmentRefs row, + @Nonnull Ref bodyRef) { + return row.bodyRef().isValid() && sameRef(row.bodyRef(), bodyRef); + } + + private static boolean isMatchingLiveRef(@Nonnull GeneratedVisualProxyRef row, + @Nonnull Ref bodyRef) { + return row.bodyRef().isValid() && sameRef(row.bodyRef(), bodyRef); + } + + @Nonnull + private Set> bodyAttachmentRefs(@Nonnull Ref bodyRef) { + int rowIndex = bodyRef.getIndex(); + BodyAttachmentRefs row = bodyAttachmentsByRowIndex.get(rowIndex); + if (row == null || !isMatchingLiveRef(row, bodyRef)) { + row = new BodyAttachmentRefs(bodyRef, new ObjectOpenHashSet<>()); + bodyAttachmentsByRowIndex.put(rowIndex, row); + } + return row.attachments(); + } + + private void unregisterAttachmentRef(@Nonnull Ref bodyRef, + @Nonnull Ref attachment) { + int rowIndex = bodyRef.getIndex(); + BodyAttachmentRefs row = bodyAttachmentsByRowIndex.get(rowIndex); + if (row == null) { + return; + } + if (!isMatchingLiveRef(row, bodyRef)) { + bodyAttachmentsByRowIndex.remove(rowIndex); + return; + } + Set> attachments = row.attachments(); + attachments.remove(attachment); + if (attachments.isEmpty()) { + bodyAttachmentsByRowIndex.remove(rowIndex); + } + } + + @Nullable + private BodyAttachmentRefs removeAttachmentRefs(@Nonnull Ref bodyRef) { + int rowIndex = bodyRef.getIndex(); + BodyAttachmentRefs row = bodyAttachmentsByRowIndex.get(rowIndex); + if (row == null || !isMatchingLiveRef(row, bodyRef)) { + if (row != null) { + bodyAttachmentsByRowIndex.remove(rowIndex); + } + return null; + } + bodyAttachmentsByRowIndex.remove(rowIndex); + return row; + } + + @Nonnull + private Collection> liveAttachments(@Nonnull Ref bodyRef) { + List> liveAttachments = new ArrayList<>(); + List> staleAttachments = new ArrayList<>(); + synchronized (this) { + int rowIndex = bodyRef.getIndex(); + BodyAttachmentRefs row = bodyAttachmentsByRowIndex.get(rowIndex); + if (row == null) { + return List.of(); + } + if (!isMatchingLiveRef(row, bodyRef)) { + bodyAttachmentsByRowIndex.remove(rowIndex); + return List.of(); + } + Set> attachments = row.attachments(); + if (attachments.isEmpty()) { + bodyAttachmentsByRowIndex.remove(rowIndex); + return List.of(); + } + for (Iterator> iterator = attachments.iterator(); iterator.hasNext();) { + Ref attachment = iterator.next(); + if (attachment != null && attachment.isValid()) { + liveAttachments.add(attachment); + } else { + iterator.remove(); + if (attachment != null) { + staleAttachments.add(attachment); + } + } + } + if (attachments.isEmpty()) { + bodyAttachmentsByRowIndex.remove(rowIndex); + } + } + cleanSyncStates(staleAttachments); + return liveAttachments; + } + + private boolean hasLiveAttachments(@Nonnull Ref bodyRef) { + boolean hasLiveAttachment = false; + List> staleAttachments = new ArrayList<>(); + synchronized (this) { + int rowIndex = bodyRef.getIndex(); + BodyAttachmentRefs row = bodyAttachmentsByRowIndex.get(rowIndex); + if (row == null) { + return false; + } + if (!isMatchingLiveRef(row, bodyRef)) { + bodyAttachmentsByRowIndex.remove(rowIndex); + return false; + } + Set> attachments = row.attachments(); + if (attachments.isEmpty()) { + bodyAttachmentsByRowIndex.remove(rowIndex); + return false; + } + for (Iterator> iterator = attachments.iterator(); iterator.hasNext();) { + Ref attachment = iterator.next(); + if (attachment != null && attachment.isValid()) { + hasLiveAttachment = true; + } else { + iterator.remove(); + if (attachment != null) { + staleAttachments.add(attachment); + } + } + } + if (attachments.isEmpty()) { + bodyAttachmentsByRowIndex.remove(rowIndex); + } + } + cleanSyncStates(staleAttachments); + return hasLiveAttachment; + } + + @Nullable + private Ref liveGeneratedVisualProxy(@Nonnull Ref bodyRef) { + Ref staleProxy = null; + synchronized (this) { + int rowIndex = bodyRef.getIndex(); + GeneratedVisualProxyRef row = generatedVisualProxiesByRowIndex.get(rowIndex); + if (row == null) { + return null; + } + if (!isMatchingLiveRef(row, bodyRef)) { + generatedVisualProxiesByRowIndex.remove(rowIndex); + return null; + } + Ref proxy = row.proxy(); + if (proxy.isValid()) { + return proxy; + } + generatedVisualProxiesByRowIndex.remove(rowIndex); + staleProxy = proxy; + } + cleanSyncState(staleProxy); + return null; + } + + @Nullable + private GeneratedVisualProxyRef removeGeneratedVisualProxyRef(@Nonnull Ref bodyRef) { + int rowIndex = bodyRef.getIndex(); + GeneratedVisualProxyRef row = generatedVisualProxiesByRowIndex.get(rowIndex); + if (row == null || !isMatchingLiveRef(row, bodyRef)) { + if (row != null) { + generatedVisualProxiesByRowIndex.remove(rowIndex); + } + return null; + } + generatedVisualProxiesByRowIndex.remove(rowIndex); + return row; + } + + @Nullable + private GeneratedVisualProxyRef removeGeneratedVisualProxyRef(@Nonnull Ref bodyRef, + @Nonnull Ref expectedProxy) { + int rowIndex = bodyRef.getIndex(); + GeneratedVisualProxyRef row = generatedVisualProxiesByRowIndex.get(rowIndex); + if (row == null || !isMatchingLiveRef(row, bodyRef)) { + if (row != null) { + generatedVisualProxiesByRowIndex.remove(rowIndex); + } + return null; + } + if (!sameRef(row.proxy(), expectedProxy)) { + return null; + } + generatedVisualProxiesByRowIndex.remove(rowIndex); + return row; + } + + private static boolean isValidRef(@Nullable Ref ref) { + return ref != null && ref.isValid(); + } + private void cleanSyncState(@Nullable Ref ref) { if (ref != null) { syncStateCleaner.accept(ref); @@ -291,9 +653,26 @@ private void cleanSyncStates(@Nonnull Collection> refs) { } } - private static boolean sameRef(@Nonnull Ref first, - @Nonnull Ref second) { - return first == second || first.equals(second); + private static boolean sameRef(@Nullable Ref first, + @Nullable Ref second) { + return first == second + || first != null + && second != null + && first.getStore() == second.getStore() + && first.getIndex() == second.getIndex(); + } + + private record BodyAttachmentRefs(@Nonnull Ref bodyRef, + @Nonnull Set> attachments) { + } + + private record GeneratedVisualProxyRef(@Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef, + @Nonnull Ref proxy) { + } + + private record BodyVisualInterestRefState(@Nonnull Ref bodyRef, + @Nonnull BodyVisualInterestState state) { } /** @@ -303,7 +682,7 @@ private static boolean sameRef(@Nonnull Ref first, * {@code VisualOcclusionMode.CULL} is enabled, so materialization and sync * share one occlusion decision window instead of spending duplicate raycasts. Raycasts are * submitted asynchronously; callers poll this state and use the last-known visibility while a - * owner query is still incomplete.

+ * store tick query is still incomplete.

*/ public static final class BodyVisualInterestState { diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldEventState.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldEventState.java deleted file mode 100644 index 4cc6b326..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldEventState.java +++ /dev/null @@ -1,204 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources; - -import dev.hytalemodding.impulse.core.plugin.events.PhysicsCommandBatchEvent; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsFrameEvent; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsSnapshotPublicationEvent; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsStepEvent; -import dev.hytalemodding.impulse.core.internal.simulation.batch.RecordedPhysicsCommandBatch; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandCompletion; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandMetadata; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandResult; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Latest value-only event frame for physics-owner outcomes. - * - *

This state intentionally replaces the previous frame instead of queueing history. The public - * event frame is a low-overhead diagnostic snapshot of the newest owner outcome and latest - * captured snapshot inclusion state.

- */ -public final class PhysicsWorldEventState { - - private final AtomicLong eventFrameSequence = new AtomicLong(); - @Nonnull - private final AtomicReference latestFrame = - new AtomicReference<>(PhysicsEventFrame.empty(0L)); - - @Nonnull - public PhysicsEventFrame getLatestFrame() { - return latestFrame.get(); - } - - @Nonnull - public PhysicsEventFrame publishCommandCompletion(long worldEpoch, - @Nonnull PublishedPhysicsSnapshotFrame latestCapturedSnapshotFrame, - @Nonnull PhysicsCommandMetadata metadata, - int commandCount, - @Nonnull PhysicsCommandCompletion completion) { - return publishCommandCompletion(worldEpoch, - latestCapturedSnapshotFrame, - metadata, - commandCount, - 0, - null, - 0, - null, - completion); - } - - @Nonnull - public PhysicsEventFrame publishCommandCompletion(long worldEpoch, - @Nonnull PublishedPhysicsSnapshotFrame latestCapturedSnapshotFrame, - @Nonnull RecordedPhysicsCommandBatch batch, - @Nonnull PhysicsCommandCompletion completion) { - Objects.requireNonNull(batch, "batch"); - return publishCommandCompletion(worldEpoch, - latestCapturedSnapshotFrame, - batch.metadata(), - batch.publicBatch().commandCount(), - batch.bodyKeyReferenceCount(), - batch.firstBodyKey(), - batch.jointKeyReferenceCount(), - batch.firstJointKey(), - completion); - } - - @Nonnull - private PhysicsEventFrame publishCommandCompletion(long worldEpoch, - @Nonnull PublishedPhysicsSnapshotFrame latestCapturedSnapshotFrame, - @Nonnull PhysicsCommandMetadata metadata, - int commandCount, - int bodyKeyReferenceCount, - @Nullable RigidBodyKey firstBodyKey, - int jointKeyReferenceCount, - @Nullable JointKey firstJointKey, - @Nonnull PhysicsCommandCompletion completion) { - Objects.requireNonNull(latestCapturedSnapshotFrame, "latestCapturedSnapshotFrame"); - Objects.requireNonNull(metadata, "metadata"); - Objects.requireNonNull(completion, "completion"); - PhysicsCommandResult firstRejected = completion.firstRejected().orElse(null); - PhysicsCommandBatchEvent commandEvent = new PhysicsCommandBatchEvent(metadata.commandBatchSequence(), - metadata.submittedServerTick(), - System.nanoTime(), - commandCount, - bodyKeyReferenceCount, - firstBodyKey, - jointKeyReferenceCount, - firstJointKey, - completion.allApplied(), - firstRejected != null ? firstRejected.commandSequence() : 0L, - firstRejected != null ? firstRejected.message() : null); - return publishFrame(worldEpoch, latestCapturedSnapshotFrame, List.of(commandEvent), List.of(), List.of()); - } - - @Nonnull - public PhysicsEventFrame publishStepCaptured(long worldEpoch, - @Nonnull PublishedPhysicsSnapshotFrame snapshotFrame) { - return publishStepCaptured(worldEpoch, snapshotFrame, List.of(), 0); - } - - @Nonnull - public PhysicsEventFrame publishStepCaptured(long worldEpoch, - @Nonnull PublishedPhysicsSnapshotFrame snapshotFrame, - @Nonnull List physicsEvents, - int droppedBackendEventCount) { - Objects.requireNonNull(snapshotFrame, "snapshotFrame"); - PhysicsStepEvent stepEvent = new PhysicsStepEvent(snapshotFrame.stepSequence(), - snapshotFrame.serverTick(), - snapshotFrame.frameEpoch(), - snapshotFrame.status(), - snapshotFrame.lastIncludedCommandBatchSequence(), - snapshotFrame.bodyCount(), - snapshotFrame.stepNanos(), - snapshotFrame.snapshotNanos()); - return publishFrame(worldEpoch, - snapshotFrame, - List.of(), - List.of(stepEvent), - List.of(), - physicsEvents, - droppedBackendEventCount); - } - - @Nonnull - public PhysicsEventFrame publishSnapshotPublication(long worldEpoch, - @Nonnull PublishedPhysicsSnapshotFrame snapshotFrame, - int appliedBodyCount) { - return publishSnapshotPublication(worldEpoch, snapshotFrame, appliedBodyCount, 0L); - } - - @Nonnull - public PhysicsEventFrame publishSnapshotPublication(long worldEpoch, - @Nonnull PublishedPhysicsSnapshotFrame snapshotFrame, - int appliedBodyCount, - long publicationServerTick) { - Objects.requireNonNull(snapshotFrame, "snapshotFrame"); - PhysicsSnapshotPublicationEvent publicationEvent = - new PhysicsSnapshotPublicationEvent(snapshotFrame.frameEpoch(), - snapshotFrame.worldEpoch(), - snapshotFrame.stepSequence(), - snapshotFrame.serverTick(), - snapshotFrame.lastIncludedCommandBatchSequence(), - publicationServerTick, - System.nanoTime(), - appliedBodyCount); - return publishFrame(worldEpoch, snapshotFrame, List.of(), List.of(), List.of(publicationEvent)); - } - - @Nonnull - public PhysicsEventFrame publishEmpty(long worldEpoch, - @Nonnull PublishedPhysicsSnapshotFrame latestCapturedSnapshotFrame) { - return publishFrame(worldEpoch, - Objects.requireNonNull(latestCapturedSnapshotFrame, "latestCapturedSnapshotFrame"), - List.of(), - List.of(), - List.of()); - } - - @Nonnull - private PhysicsEventFrame publishFrame(long worldEpoch, - @Nonnull PublishedPhysicsSnapshotFrame latestCapturedSnapshotFrame, - @Nonnull List commandEvents, - @Nonnull List stepEvents, - @Nonnull List publicationEvents) { - return publishFrame(worldEpoch, - latestCapturedSnapshotFrame, - commandEvents, - stepEvents, - publicationEvents, - List.of(), - 0); - } - - @Nonnull - private PhysicsEventFrame publishFrame(long worldEpoch, - @Nonnull PublishedPhysicsSnapshotFrame latestCapturedSnapshotFrame, - @Nonnull List commandEvents, - @Nonnull List stepEvents, - @Nonnull List publicationEvents, - @Nonnull List physicsEvents, - int droppedBackendEventCount) { - PhysicsEventFrame frame = new PhysicsEventFrame(eventFrameSequence.incrementAndGet(), - worldEpoch, - latestCapturedSnapshotFrame.frameEpoch(), - latestCapturedSnapshotFrame.stepSequence(), - latestCapturedSnapshotFrame.serverTick(), - latestCapturedSnapshotFrame.lastIncludedCommandBatchSequence(), - commandEvents, - stepEvents, - publicationEvents, - physicsEvents, - droppedBackendEventCount); - latestFrame.set(frame); - return frame; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldLifecycleState.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldLifecycleState.java deleted file mode 100644 index 3eeab8a5..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldLifecycleState.java +++ /dev/null @@ -1,241 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources; - -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistration; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistry; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodySnapshotVisitor; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsCommandVisibilityState; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldSnapshotState.ApplyResult; -import dev.hytalemodding.impulse.core.internal.simulation.batch.RecordedPhysicsCommandBatch; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsFrameEvent; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandCompletion; -import dev.hytalemodding.impulse.core.plugin.snapshot.PhysicsBodySnapshotEntry; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import java.util.Collection; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Function; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3f; - -/** - * Coordinates world lifecycle state that becomes visible across physics frame boundaries. - */ -public final class PhysicsWorldLifecycleState { - - private final PhysicsWorldSnapshotState snapshotState = new PhysicsWorldSnapshotState(); - private final PhysicsWorldEventState eventState = new PhysicsWorldEventState(); - private final PhysicsCommandVisibilityState commandVisibility = new PhysicsCommandVisibilityState(); - - public long worldEpoch() { - return snapshotState.worldEpoch(); - } - - public long commandWorldEpoch() { - return commandVisibility.commandWorldEpoch(); - } - - public long nextCommandBatchSequence() { - return commandVisibility.nextCommandBatchSequence(); - } - - @Nonnull - public PhysicsEventFrame latestEventFrame() { - return eventState.getLatestFrame(); - } - - @Nullable - public PhysicsBodySnapshot getBodySnapshot(@Nonnull RigidBodyKey bodyKey) { - return snapshotState.getBodySnapshot(bodyKey); - } - - @Nonnull - public PhysicsBodySnapshot captureBodySnapshot(@Nonnull PhysicsBodyRegistration registration) { - return snapshotState.captureBodySnapshot(registration); - } - - public void putBodySnapshot(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - snapshotState.putBodySnapshot(bodyKey, snapshot, spaceId, kind, persistenceMode); - } - - @Nonnull - public PublishedPhysicsSnapshotFrame capturePublishedSnapshotFrame( - @Nonnull Collection spaces, - @Nonnull PhysicsBodyRegistry bodyRegistry, - long stepSequence, - long serverTick, - @Nonnull PublishedPhysicsSnapshotFrame.Status status, - long stepNanos, - boolean profilingEnabled) { - return capturePublishedSnapshotFrame(spaces, - bodyRegistry, - stepSequence, - serverTick, - status, - stepNanos, - profilingEnabled, - List.of(), - 0); - } - - @Nonnull - public PublishedPhysicsSnapshotFrame capturePublishedSnapshotFrame( - @Nonnull Collection spaces, - @Nonnull PhysicsBodyRegistry bodyRegistry, - long stepSequence, - long serverTick, - @Nonnull PublishedPhysicsSnapshotFrame.Status status, - long stepNanos, - boolean profilingEnabled, - @Nonnull List physicsEvents, - int droppedBackendEventCount) { - PublishedPhysicsSnapshotFrame frame = snapshotState.capturePublishedSnapshotFrame(spaces, - bodyRegistry, - stepSequence, - serverTick, - commandVisibility.completedCommandBatchSequence(), - status, - stepNanos, - profilingEnabled); - eventState.publishStepCaptured(frame.worldEpoch(), - frame, - physicsEvents, - droppedBackendEventCount); - return frame; - } - - public int applyPublishedSnapshotFrame(@Nonnull PublishedPhysicsSnapshotFrame frame, - @Nonnull PhysicsBodyRegistry bodyRegistry, - long publicationServerTick) { - ApplyResult result = snapshotState.applyPublishedSnapshotFrame(frame); - if (result.currentWorldEpoch()) { - bodyRegistry.applyPublishedRegistrationFrame(frame); - commandVisibility.applyLastIncludedCommandBatchSequence( - frame.lastIncludedCommandBatchSequence()); - eventState.publishSnapshotPublication(snapshotState.worldEpoch(), - frame, - result.appliedCount(), - publicationServerTick); - } - return result.appliedCount(); - } - - @Nonnull - public PublishedPhysicsSnapshotFrame latestPublishedFrame() { - return snapshotState.getLatestPublishedFrame(); - } - - public long latestSnapshotAppliedNanos() { - return snapshotState.getLatestSnapshotAppliedNanos(); - } - - public int bodySnapshotCount() { - return snapshotState.getBodySnapshotCount(); - } - - public int bodySnapshotCount(@Nonnull SpaceId spaceId) { - return snapshotState.getBodySnapshotCount(spaceId); - } - - public int bodySnapshotCellCount() { - return snapshotState.getBodySnapshotCellCount(); - } - - public void forEachBodySnapshot(@Nonnull SpaceId spaceId, - @Nonnull Consumer consumer) { - snapshotState.forEachBodySnapshot(spaceId, consumer); - } - - public void forEachIndexedBodySnapshot(@Nonnull SpaceId spaceId, - @Nonnull PhysicsBodySnapshotVisitor visitor) { - snapshotState.forEachIndexedBodySnapshot(spaceId, visitor); - } - - public int forEachBodySnapshotNear(@Nonnull SpaceId spaceId, - @Nonnull Vector3f center, - float radius, - @Nonnull Consumer consumer) { - return snapshotState.forEachBodySnapshotNear(spaceId, center, radius, consumer); - } - - public int forEachIndexedBodySnapshotNear(@Nonnull SpaceId spaceId, - @Nonnull Vector3f center, - float radius, - @Nonnull PhysicsBodySnapshotVisitor visitor) { - return snapshotState.forEachIndexedBodySnapshotNear(spaceId, center, radius, visitor); - } - - public void removeBodySnapshot(@Nonnull RigidBodyKey bodyKey) { - snapshotState.removeBodySnapshot(bodyKey); - } - - public void clearBodySnapshots() { - snapshotState.clearBodySnapshots(); - } - - public boolean isBodyCreationPending(@Nonnull RigidBodyKey bodyKey, - boolean directBodyCreationPending, - boolean ownerExecutorAttached) { - return commandVisibility.isBodyCreationPending(bodyKey, directBodyCreationPending); - } - - public boolean trackBodyCreationPublication(@Nonnull RecordedPhysicsCommandBatch batch, - boolean ownerExecutorAttached) { - return commandVisibility.trackBodyCreationPublication(batch, ownerExecutorAttached); - } - - public void clearBodyCreationPublication(@Nonnull RecordedPhysicsCommandBatch batch) { - commandVisibility.clearBodyCreationPublication(batch); - } - - public void clearBodyCreationPending(@Nonnull RigidBodyKey bodyKey) { - commandVisibility.clearBodyCreationPending(bodyKey); - } - - public void publishDetachedOwnerRegistrationViews(@Nonnull PhysicsBodyRegistry bodyRegistry) { - bodyRegistry.publishLiveRegistrationViews(); - commandVisibility.applyLastIncludedCommandBatchSequence( - commandVisibility.completedCommandBatchSequence()); - } - - public void markWorldChanged(@Nonnull PhysicsBodyRegistry bodyRegistry, - boolean ownerExecutorAttached) { - snapshotState.markWorldChanged(); - if (!ownerExecutorAttached) { - bodyRegistry.publishLiveRegistrationViews(); - } - if (commandVisibility.markWorldChanged()) { - eventState.publishEmpty(snapshotState.worldEpoch(), snapshotState.getLatestPublishedFrame()); - } - } - - @Nonnull - public PhysicsCommandCompletion executeCommandBatch(@Nonnull RecordedPhysicsCommandBatch batch, - @Nonnull Function executor) { - Objects.requireNonNull(batch, "batch"); - Objects.requireNonNull(executor, "executor"); - int depth = commandVisibility.enterCommandBatchExecution(); - try { - PhysicsCommandCompletion completion = executor.apply(batch); - commandVisibility.markCommandBatchCompleted(batch.metadata().commandBatchSequence()); - eventState.publishCommandCompletion(snapshotState.worldEpoch(), - snapshotState.getLatestPublishedFrame(), - batch, - completion); - return completion; - } finally { - commandVisibility.exitCommandBatchExecution(depth); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldRuntimeResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldRuntimeResource.java deleted file mode 100644 index cf333fc1..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldRuntimeResource.java +++ /dev/null @@ -1,1501 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources; - -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.logger.HytaleLogger; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; -import dev.hytalemodding.impulse.core.internal.modules.control.ControlLifecycle; -import dev.hytalemodding.impulse.core.internal.modules.control.PhysicsControlRuntimeState; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistry; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRuntimeState.BodySyncState; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRuntime; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRuntimeState; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodySnapshots; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodySnapshotVisitor; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.PhysicsChunkBoundaryRuntime; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.PhysicsChunkBoundaryRuntime.ChunkBoundaryPauseState; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.PhysicsChunkBoundaryRuntime.ChunkBoundarySafeState; -import dev.hytalemodding.impulse.core.internal.resources.joint.PhysicsJointRegistration; -import dev.hytalemodding.impulse.core.internal.resources.joint.PhysicsJointRegistry; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerCallable; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerGateway; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerHandle; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerMutation; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime.BodyVisualInterestState; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime.VisualInterest; -import dev.hytalemodding.impulse.core.internal.simulation.recorder.MutablePhysicsCommandContext; -import dev.hytalemodding.impulse.core.internal.simulation.query.PhysicsInternalQuery; -import dev.hytalemodding.impulse.core.internal.simulation.PhysicsSimulationExecutor; -import dev.hytalemodding.impulse.core.internal.simulation.batch.RecordedPhysicsCommandBatch; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldCollisionBuildOptions; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.PhysicsWorldCollisionRuntime; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldCollisionLifecycle; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldVoxelCollisionCache; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistration; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyRegistrationView; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionBuildStats; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionMode; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionPrewarmStats; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionStats; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsFrameEvent; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldCollisionSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.JointType; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandCompletion; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandHandle; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandRecipe; -import dev.hytalemodding.impulse.core.plugin.simulation.query.PhysicsQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.query.PhysicsQueryHandle; -import dev.hytalemodding.impulse.core.plugin.snapshot.PhysicsBodySnapshotEntry; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Consumer; -import java.util.logging.Level; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Quaternionf; -import org.joml.Vector3d; -import org.joml.Vector3f; - -/** - * Internal ECS resource implementation behind {@link PhysicsWorldResource}. - */ -public class PhysicsWorldRuntimeResource extends PhysicsWorldResource { - - private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); - - private final PhysicsSpaceRuntime spaceRuntime = new PhysicsSpaceRuntime(); - - private final PhysicsBodyRegistry bodyRegistry = new PhysicsBodyRegistry(); - - private final PhysicsWorldCollisionRuntime collisionRuntime = - new PhysicsWorldCollisionRuntime(); - - @Nonnull - private final PhysicsSimulationRuntime simulationRuntime = new PhysicsSimulationRuntime(); - - private final PhysicsBodyRuntimeState runtimeState = new PhysicsBodyRuntimeState(); - private final PhysicsControlRuntimeState controlRuntime = new PhysicsControlRuntimeState(); - private final PhysicsJointRegistry jointRegistry = new PhysicsJointRegistry(); - private final PhysicsChunkBoundaryRuntime chunkRuntime = new PhysicsChunkBoundaryRuntime(); - private final PhysicsVisualRuntime visualRuntime = new PhysicsVisualRuntime(this::clearBodySyncState); - private final PhysicsWorldLifecycleState lifecycleState = new PhysicsWorldLifecycleState(); - private final PhysicsBodyRuntime bodyRuntime = new PhysicsBodyRuntime(spaceRuntime, - bodyRegistry, - runtimeState, - controlRuntime, - jointRegistry, - chunkRuntime, - visualRuntime, - lifecycleState, - this::markWorldChanged); - - private final AtomicLong visualInterestTick = new AtomicLong(); - private final PhysicsOwnerGateway ownerGateway = new PhysicsOwnerGateway(); - private final PhysicsSimulationExecutor simulationExecutor = new PhysicsSimulationExecutor(this); - - public PhysicsWorldRuntimeResource() { - ControlLifecycle.registerResource(this); - WorldCollisionLifecycle.registerResource(this); - } - - @Nonnull - public static PhysicsWorldRuntimeResource require(@Nonnull Store store) { - return require(store.getResource(PhysicsWorldResource.getResourceType())); - } - - @Nonnull - public static PhysicsWorldRuntimeResource require(@Nonnull PhysicsWorldResource resource) { - if (resource instanceof PhysicsWorldRuntimeResource runtime) { - return runtime; - } - throw new IllegalStateException( - "Physics world resource is not the Impulse runtime implementation"); - } - - public void attachOwnerExecutor(@Nonnull PhysicsOwnerHandle ownerExecutor) { - ownerGateway.attachOwnerExecutor(ownerExecutor); - } - - public void detachOwnerExecutor(@Nonnull PhysicsOwnerHandle ownerExecutor) { - ownerGateway.detachOwnerExecutor(ownerExecutor); - if (!ownerGateway.hasOwnerExecutor()) { - lifecycleState.publishDetachedOwnerRegistrationViews(bodyRegistry); - } - } - - public boolean canAccessLiveBackendDirectly() { - return ownerGateway.canAccessLiveBackendDirectly(); - } - - public void rejectSynchronousCompletionCallbackWait(@Nonnull String operation) { - ownerGateway.rejectSynchronousCompletionCallbackWait(operation); - } - - public long worldEpoch() { - return lifecycleState.worldEpoch(); - } - - public long commandWorldEpoch() { - return lifecycleState.commandWorldEpoch(); - } - - @Nonnull - public MutablePhysicsCommandContext createMutableCommandContext(long submittedServerTick) { - return new MutablePhysicsCommandContext(submittedServerTick, - lifecycleState.commandWorldEpoch()); - } - - @Nonnull - public MutablePhysicsCommandContext createMutableCommandContext(long submittedServerTick, - int expectedOperations) { - return new MutablePhysicsCommandContext(submittedServerTick, - lifecycleState.commandWorldEpoch(), - expectedOperations); - } - - @Nonnull - @Override - public PhysicsCommandHandle submitCommands(long submittedServerTick, - @Nonnull PhysicsCommandRecipe recipe) { - MutablePhysicsCommandContext context = createMutableCommandContext(submittedServerTick); - context.compose(recipe); - return submitRecordedCommands(context); - } - - @Nonnull - @Override - public PhysicsCommandHandle submitCommands(long submittedServerTick, - int expectedOperations, - @Nonnull PhysicsCommandRecipe recipe) { - MutablePhysicsCommandContext context = - createMutableCommandContext(submittedServerTick, expectedOperations); - context.compose(recipe); - return submitRecordedCommands(context); - } - - @Nonnull - public PhysicsCommandHandle submitRecordedCommands(@Nonnull MutablePhysicsCommandContext context) { - Objects.requireNonNull(context, "context"); - RecordedPhysicsCommandBatch batch = - context.freezeInternal(lifecycleState.nextCommandBatchSequence()); - /* - * Body creation through commands publishes registration views with the next snapshot frame. - * Until that frame is applied, sync/materialization must treat creation as pending even - * though the owner may already have completed the command batch. - */ - boolean trackBodyCreationPublication = trackCommandBodyCreationPublication(batch); - CompletableFuture completion = - ownerGateway.enqueueCall("execute physics command batch", () -> executeCommandBatch(batch)); - if (trackBodyCreationPublication) { - completion.whenComplete((ignored, failure) -> { - if (failure != null) { - clearCommandBodyCreationPublication(batch); - } - }); - } - return PhysicsCommandHandle.fromCompletionSummary(batch.publicBatch(), completion); - } - - @Nonnull - @Override - public PhysicsQueryHandle query(@Nonnull PhysicsQuery query) { - Objects.requireNonNull(query, "query"); - CompletableFuture completion = - ownerGateway.enqueueCall("execute physics query", () -> simulationExecutor.query(query)); - return PhysicsQueryHandle.fromCompletion(query, completion); - } - - @Nonnull - public CompletionStage queryInternal(@Nonnull PhysicsInternalQuery query) { - Objects.requireNonNull(query, "query"); - CompletableFuture completion = - ownerGateway.enqueueCall("execute internal physics query", - () -> simulationExecutor.queryInternal(query)); - return completion.minimalCompletionStage(); - } - - @Nonnull - @Override - public PhysicsEventFrame getLatestEventFrame() { - return lifecycleState.latestEventFrame(); - } - - public void assertCanAccessLiveBackendDirectly(@Nonnull String operation) { - ownerGateway.assertCanAccessLiveBackendDirectly(operation); - } - - public void runOwnerMutation(@Nonnull String operation, - @Nonnull PhysicsOwnerMutation mutation) { - ownerGateway.run(operation, mutation); - } - - @Nonnull - public PhysicsMutationHandle enqueueOwnerMutation(@Nonnull String operation, - @Nonnull PhysicsOwnerMutation mutation) { - return enqueueOwnerMutation(operation, null, mutation); - } - - - @Nonnull - public PhysicsMutationHandle enqueueOwnerMutation(@Nonnull String operation, - @Nullable T value, - @Nonnull PhysicsOwnerMutation mutation) { - return ownerGateway.enqueue(operation, value, mutation); - } - - - @Nonnull - public T callOwner(@Nonnull String operation, - @Nonnull PhysicsOwnerCallable callable) { - return ownerGateway.call(operation, callable); - } - - - @Nonnull - @Override - public PhysicsWorldSettings getWorldSettings() { - return simulationRuntime.getWorldSettings(); - } - - @Override - public void setWorldSettings(@Nonnull PhysicsWorldSettings settings) { - PhysicsWorldSettings requested = new PhysicsWorldSettings(settings); - runOwnerMutation("set physics world settings", () -> setWorldSettingsDirect(requested)); - } - - @Nonnull - @Override - public PhysicsMutationHandle setWorldSettingsAsync( - @Nonnull PhysicsWorldSettings settings) { - PhysicsWorldSettings requested = new PhysicsWorldSettings(settings); - return enqueueOwnerMutation("set physics world settings", - () -> setWorldSettingsDirect(requested)); - } - - private void setWorldSettingsDirect(@Nonnull PhysicsWorldSettings settings) { - validateStepModeSupported(settings.getStepMode()); - simulationRuntime.setWorldSettings(settings); - } - - @Nonnull - @Override - public SpaceId createSpace(@Nonnull BackendId backendId) { - return createSpace(backendId, "", PhysicsSpaceSettings.defaults()); - } - - @Nonnull - @Override - public SpaceId createSpace(@Nonnull BackendId backendId, @Nonnull String worldName) { - return createSpace(backendId, worldName, PhysicsSpaceSettings.defaults()); - } - - @Nonnull - @Override - public SpaceId createSpace(@Nonnull BackendId backendId, - @Nonnull String worldName, - @Nonnull PhysicsSpaceSettings settings) { - return createSpace(backendId, SpaceId.next(), worldName, settings); - } - - @Nonnull - @Override - public SpaceId createSpace(@Nonnull BackendId backendId, - @Nonnull SpaceId spaceId, - @Nonnull String worldName, - @Nonnull PhysicsSpaceSettings settings) { - callOwner("create physics space", - () -> createSpaceDirect(backendId, spaceId, worldName, settings)); - return spaceId; - } - - @Nonnull - @Override - public PhysicsMutationHandle createSpaceAsync(@Nonnull BackendId backendId, - @Nonnull String worldName, - @Nonnull PhysicsSpaceSettings settings) { - SpaceId spaceId = SpaceId.next(); - return createSpaceAsync(backendId, spaceId, worldName, settings); - } - - @Nonnull - @Override - public PhysicsMutationHandle createSpaceAsync(@Nonnull BackendId backendId, - @Nonnull SpaceId spaceId, - @Nonnull String worldName, - @Nonnull PhysicsSpaceSettings settings) { - return enqueueOwnerMutation("create physics space", - spaceId, - () -> createSpaceDirect(backendId, spaceId, worldName, settings)); - } - - @Nonnull - private PhysicsSpaceBinding createSpaceDirect(@Nonnull BackendId backendId, - @Nonnull SpaceId spaceId, - @Nonnull String worldName, - @Nonnull PhysicsSpaceSettings settings) { - PhysicsSpaceBinding binding = spaceRuntime.createSpace(backendId, - spaceId, - worldName, - settings, - simulationRuntime.getWorldSettings().getStepMode()); - collisionRuntime.registerSpace(spaceId); - markWorldChanged(); - return binding; - } - - @Nullable - public PhysicsSpaceBinding getSpaceBinding(@Nonnull SpaceId spaceId) { - return spaceRuntime.getBinding(spaceId); - } - - @Override - public boolean hasSpace(@Nonnull SpaceId spaceId) { - return spaceRuntime.getBinding(spaceId) != null; - } - - @Nonnull - public PhysicsSpaceBinding requireSpaceBinding(@Nonnull SpaceId spaceId) { - return spaceRuntime.requireBinding(spaceId); - } - - @Nonnull - public Collection getSpaceBindings() { - return spaceRuntime.getBindings(); - } - - @Nonnull - @Override - public Collection getSpaceIds() { - return spaceRuntime.getSpaceIds(); - } - - @Override - public int getSpaceCount() { - return spaceRuntime.getSpaceCount(); - } - - /** - * Iterate spaces without allocating a snapshot collection. - * Use this from tick systems that do not mutate the space map while iterating. - */ - @Nonnull - public Iterable iterateSpaceBindings() { - return spaceRuntime.iterateBindings(); - } - - @Override - public int refreshBodySnapshots() { - return callOwner("refresh physics body snapshots", () -> { - PublishedPhysicsSnapshotFrame frame = capturePublishedSnapshotFrameDirect(0L, - 0L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - return applyPublishedSnapshotFrame(frame); - }); - } - - @Nonnull - @Override - public PhysicsBodySnapshot getBodySnapshot(@Nonnull RigidBodyKey bodyKey) { - PhysicsBodySnapshot snapshot = lifecycleState.getBodySnapshot(bodyKey); - if (snapshot != null) { - return snapshot; - } - return callOwner("refresh missing physics body snapshot", - () -> getBodySnapshotDirect(bodyKey)); - } - - @Nullable - public PhysicsBodySnapshot getBodySnapshotIfRegistered(@Nonnull RigidBodyKey bodyKey) { - PhysicsBodySnapshot snapshot = lifecycleState.getBodySnapshot(bodyKey); - if (snapshot != null) { - return snapshot; - } - return callOwner("refresh optional physics body snapshot", - () -> getBodySnapshotIfRegisteredDirect(bodyKey)); - } - - @Nonnull - private PhysicsBodySnapshot getBodySnapshotDirect(@Nonnull RigidBodyKey bodyKey) { - PhysicsBodySnapshot snapshot = lifecycleState.getBodySnapshot(bodyKey); - if (snapshot != null) { - return snapshot; - } - PhysicsBodyRegistration registration = requireBodyRegistration(bodyKey); - return captureLiveBodySnapshot(registration); - } - - @Nullable - private PhysicsBodySnapshot getBodySnapshotIfRegisteredDirect(@Nonnull RigidBodyKey bodyKey) { - PhysicsBodySnapshot snapshot = lifecycleState.getBodySnapshot(bodyKey); - if (snapshot != null) { - return snapshot; - } - PhysicsBodyRegistration registration = bodyRegistry.getRegistration(bodyKey); - return registration != null ? captureLiveBodySnapshot(registration) : null; - } - - @Nonnull - public PhysicsBodySnapshot captureLiveBodySnapshot(@Nonnull PhysicsBodyRegistration registration) { - Objects.requireNonNull(registration, "registration"); - assertCanAccessLiveBackendDirectly("capture live physics body snapshot"); - PhysicsSpaceBinding space = requireSpaceBinding(registration.spaceId()); - PhysicsBodySnapshot snapshot = PhysicsBodySnapshots.read(space, - registration.backendBodyHandle().value()); - if (snapshot == null) { - throw new IllegalStateException( - "No live physics body snapshot is available for " + registration.bodyKey()); - } - return snapshot; - } - - /** - * Captures an immutable snapshot frame on the physics owner lane. - * - *

The generated {@code frameEpoch} and current {@code worldEpoch} govern - * publication ordering and stale-frame rejection. {@code stepSequence} and - * {@code serverTick} are copied through as external correlation metadata.

- */ - @Nonnull - public PublishedPhysicsSnapshotFrame capturePublishedSnapshotFrame(long stepSequence, - long serverTick, - @Nonnull PublishedPhysicsSnapshotFrame.Status status, - long stepNanos, - boolean profilingEnabled) { - return capturePublishedSnapshotFrame(stepSequence, - serverTick, - status, - stepNanos, - profilingEnabled, - List.of(), - 0); - } - - @Nonnull - public PublishedPhysicsSnapshotFrame capturePublishedSnapshotFrame(long stepSequence, - long serverTick, - @Nonnull PublishedPhysicsSnapshotFrame.Status status, - long stepNanos, - boolean profilingEnabled, - @Nonnull List physicsEvents, - int droppedBackendEventCount) { - return callOwner("capture published physics snapshot frame", - () -> capturePublishedSnapshotFrameDirect(stepSequence, - serverTick, - status, - stepNanos, - profilingEnabled, - physicsEvents, - droppedBackendEventCount)); - } - - @Nonnull - private PublishedPhysicsSnapshotFrame capturePublishedSnapshotFrameDirect(long stepSequence, - long serverTick, - @Nonnull PublishedPhysicsSnapshotFrame.Status status, - long stepNanos, - boolean profilingEnabled) { - return capturePublishedSnapshotFrameDirect(stepSequence, - serverTick, - status, - stepNanos, - profilingEnabled, - List.of(), - 0); - } - - @Nonnull - private PublishedPhysicsSnapshotFrame capturePublishedSnapshotFrameDirect(long stepSequence, - long serverTick, - @Nonnull PublishedPhysicsSnapshotFrame.Status status, - long stepNanos, - boolean profilingEnabled, - @Nonnull List physicsEvents, - int droppedBackendEventCount) { - - assertCanAccessLiveBackendDirectly("capture published physics snapshot frame"); - return lifecycleState.capturePublishedSnapshotFrame(spaceRuntime.getBindings(), - bodyRegistry, - stepSequence, - serverTick, - status, - stepNanos, - profilingEnabled, - physicsEvents, - droppedBackendEventCount); - } - - public int applyPublishedSnapshotFrame(@Nonnull PublishedPhysicsSnapshotFrame frame) { - return applyPublishedSnapshotFrame(frame, 0L); - } - - public int applyPublishedSnapshotFrame(@Nonnull PublishedPhysicsSnapshotFrame frame, - long publicationServerTick) { - return lifecycleState.applyPublishedSnapshotFrame(frame, bodyRegistry, publicationServerTick); - } - - @Nonnull - public PublishedPhysicsSnapshotFrame getLatestPublishedFrame() { - return lifecycleState.latestPublishedFrame(); - } - - public long getLatestSnapshotAppliedNanos() { - return lifecycleState.latestSnapshotAppliedNanos(); - } - - @Override - public int getBodySnapshotCount() { - return lifecycleState.bodySnapshotCount(); - } - - @Override - public int getBodySnapshotCount(@Nonnull SpaceId spaceId) { - return lifecycleState.bodySnapshotCount(spaceId); - } - - @Override - public int getBodySnapshotCellCount() { - return lifecycleState.bodySnapshotCellCount(); - } - - @Nonnull - public WorldVoxelCollisionCache worldCollisionCache() { - return collisionRuntime.worldVoxelCollisionCache(); - } - - @Nonnull - @Override - public WorldCollisionBuildStats rebuildWorldCollisionAround(@Nonnull World world, - @Nonnull SpaceId spaceId, - @Nonnull Vector3d center, - int radius) { - requireWorldCollisionLifecycleEnabled(); - return callOwner("rebuild world collision", () -> { - PhysicsSpaceBinding space = requireSpaceBinding(spaceId); - requireWorldCollisionSpaceEnabled(spaceId); - WorldCollisionBuildOptions buildOptions = - WorldCollisionBuildOptions.fromSettings(getLiveSpaceSettings(spaceId) - .getWorldCollisionSettings()); - return collisionRuntime.rebuildAround(world, - space, - center, - radius, - buildOptions); - }); - } - - @Nonnull - @Override - public WorldCollisionBuildStats refreshWorldCollisionAround(@Nonnull World world, - @Nonnull SpaceId spaceId, - @Nonnull Vector3d center, - int radius) { - requireWorldCollisionLifecycleEnabled(); - return callOwner("refresh world collision", () -> { - PhysicsSpaceBinding space = requireSpaceBinding(spaceId); - requireWorldCollisionSpaceEnabled(spaceId); - WorldCollisionBuildOptions buildOptions = - WorldCollisionBuildOptions.fromSettings(getLiveSpaceSettings(spaceId) - .getWorldCollisionSettings()); - return collisionRuntime.refreshAround(world, - space, - center, - radius, - buildOptions); - }); - } - - @Nonnull - @Override - public WorldCollisionPrewarmStats ensureWorldCollisionAround(@Nonnull World world, - @Nonnull SpaceId spaceId, - @Nonnull Iterable centers, - int radius, - long tick) { - Objects.requireNonNull(centers, "centers"); - requireWorldCollisionLifecycleEnabled(); - return callOwner("ensure world collision", () -> { - PhysicsSpaceBinding space = requireSpaceBinding(spaceId); - requireWorldCollisionSpaceEnabled(spaceId); - WorldCollisionBuildOptions buildOptions = - WorldCollisionBuildOptions.fromSettings(getLiveSpaceSettings(spaceId) - .getWorldCollisionSettings()); - return collisionRuntime.ensureAround(world, - space, - centers, - radius, - tick, - buildOptions); - }); - } - - @Override - public int clearWorldCollision(@Nonnull SpaceId spaceId) { - return callOwner("clear world collision", () -> { - PhysicsSpaceBinding space = requireSpaceBinding(spaceId); - return collisionRuntime.clear(space); - }); - } - - public long worldCollisionStreamingRevision(@Nonnull SpaceId spaceId) { - return collisionRuntime.streamingRevision(spaceId); - } - - @Nonnull - @Override - public WorldCollisionStats getWorldCollisionStats() { - return callOwner("read world collision stats", collisionRuntime::getStats); - } - - public void disableWorldCollisionLifecycle() { - try { - runOwnerMutation("disable world collision lifecycle", this::disableWorldCollisionLifecycleDirect); - } catch (RejectedExecutionException ignored) { - // The server can unload the subplugin after a world owner lane has already closed. - } catch (RuntimeException exception) { - LOGGER.at(Level.WARNING).log("Failed to disable world collision lifecycle: %s", - exception.getMessage()); - } - } - - private void disableWorldCollisionLifecycleDirect() { - collisionRuntime.clearRetainedTerrain(spaceRuntime.getBindings()); - restoreCollisionLodFiltersDirect(); - restoreChunkBoundaryPausedBodiesDirect(); - chunkRuntime.clearChunkBoundaryStates(); - } - - private void restoreCollisionLodFiltersDirect() { - int fullDynamicMask = PhysicsCollisionFilters.TERRAIN - | PhysicsCollisionFilters.DYNAMIC_BODY; - for (PhysicsBodyRegistration registration : bodyRegistry.getRegistrations(PhysicsBodyKind.BODY)) { - PhysicsSpaceBinding space = getSpaceBinding(registration.spaceId()); - if (space == null) { - continue; - } - space.runtime().setBodyCollisionFilter(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value(), - PhysicsCollisionFilters.DYNAMIC_BODY, - fullDynamicMask); - space.runtime().activateBody(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value()); - } - } - - private void restoreChunkBoundaryPausedBodiesDirect() { - for (RigidBodyKey bodyKey : chunkRuntime.getChunkBoundaryPausedBodyKeys()) { - ChunkBoundaryPauseState pauseState = chunkRuntime.getChunkBoundaryPauseState(bodyKey); - PhysicsBodyRegistration registration = getRegistration(bodyKey); - if (pauseState == null || registration == null) { - chunkRuntime.clearChunkBoundaryPauseState(bodyKey); - continue; - } - PhysicsSpaceBinding space = getSpaceBinding(registration.spaceId()); - if (space == null) { - chunkRuntime.clearChunkBoundaryPauseState(bodyKey); - continue; - } - space.runtime().setBodyType(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value(), - BackendRuntimeCodes.bodyTypeCode(pauseState.getOriginalBodyType())); - space.runtime().setBodyVelocity(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value(), - pauseState.getLinearVelocity().x, - pauseState.getLinearVelocity().y, - pauseState.getLinearVelocity().z, - pauseState.getAngularVelocity().x, - pauseState.getAngularVelocity().y, - pauseState.getAngularVelocity().z); - space.runtime().activateBody(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value()); - chunkRuntime.clearChunkBoundaryPauseState(bodyKey); - } - } - - private static void requireWorldCollisionLifecycleEnabled() { - if (!WorldCollisionLifecycle.isEnabled()) { - throw new IllegalStateException("Impulse world collision subplugin is disabled"); - } - } - - private void requireWorldCollisionSpaceEnabled(@Nonnull SpaceId spaceId) { - if (getLiveSpaceSettings(spaceId).getWorldCollisionSettings().getWorldCollisionMode() - == WorldCollisionMode.NONE) { - throw new IllegalStateException("World collision is disabled for space " + spaceId); - } - } - - @Override - public void forEachBodySnapshot(@Nonnull SpaceId spaceId, - @Nonnull Consumer consumer) { - lifecycleState.forEachBodySnapshot(spaceId, consumer); - } - - public void forEachIndexedBodySnapshot(@Nonnull SpaceId spaceId, - @Nonnull PhysicsBodySnapshotVisitor visitor) { - lifecycleState.forEachIndexedBodySnapshot(spaceId, visitor); - } - - @Override - public int forEachBodySnapshotNear(@Nonnull SpaceId spaceId, - @Nonnull Vector3f center, - float radius, - @Nonnull Consumer consumer) { - return lifecycleState.forEachBodySnapshotNear(spaceId, center, radius, consumer); - } - - public int forEachIndexedBodySnapshotNear(@Nonnull SpaceId spaceId, - @Nonnull Vector3f center, - float radius, - @Nonnull PhysicsBodySnapshotVisitor visitor) { - return lifecycleState.forEachIndexedBodySnapshotNear(spaceId, center, radius, visitor); - } - - @Override - public void removeSpace(@Nonnull SpaceId spaceId) { - removeSpace(spaceId, ""); - } - - @Override - public void removeSpace(@Nonnull SpaceId spaceId, @Nonnull String worldName) { - runOwnerMutation("remove physics space", () -> removeSpaceDirect(spaceId, worldName)); - } - - @Nonnull - @Override - public PhysicsMutationHandle removeSpaceAsync(@Nonnull SpaceId spaceId, - @Nonnull String worldName) { - return enqueueOwnerMutation("remove physics space", - spaceId, - () -> removeSpaceDirect(spaceId, worldName)); - } - - private void removeSpaceDirect(@Nonnull SpaceId spaceId, @Nonnull String worldName) { - PhysicsSpaceBinding removed = spaceRuntime.removeSpace(spaceId); - if (removed == null) { - collisionRuntime.clear(spaceId, null); - return; - } - - try { - collisionRuntime.clear(spaceId, removed); - jointRegistry.unregisterSpace(spaceId); - for (PhysicsBodyRegistration registration : new ArrayList<>(bodyRegistry.getRegistrations())) { - if (registration.spaceId().equals(spaceId)) { - destroyBody(registration.bodyKey(), false); - } - } - LOGGER.at(Level.FINE).log( - "World %s removed physics space id=%s backend=%s", - worldName, - removed.spaceId(), - removed.backendId()); - markWorldChanged(); - } finally { - PhysicsSpaceRuntime.closeBindingSilently(removed, worldName, "removed physics space"); - } - } - - @Override - public void clearAllSpaces(@Nonnull String worldName) { - runOwnerMutation("clear physics spaces", () -> clearAllSpacesDirect(worldName)); - } - - @Nonnull - @Override - public PhysicsMutationHandle clearAllSpacesAsync(@Nonnull String worldName) { - return enqueueOwnerMutation("clear physics spaces", - () -> clearAllSpacesDirect(worldName)); - } - - private void clearAllSpacesDirect(@Nonnull String worldName) { - RuntimeException failure = null; - for (SpaceId spaceId : spaceRuntime.getSpaceIds()) { - try { - removeSpaceDirect(spaceId, worldName); - } catch (RuntimeException exception) { - failure = collectFailure(failure, exception); - } - } - if (failure != null) { - throw failure; - } - } - - @Nonnull - private static RuntimeException collectFailure(@Nullable RuntimeException failure, - @Nonnull RuntimeException exception) { - if (failure == null) { - return exception; - } - failure.addSuppressed(exception); - return failure; - } - - /** - * Clears runtime physics state by replacing each native backend space with an empty - * space that keeps the same logical id, backend, settings, and gravity. - */ - @Nonnull - public PhysicsRuntimeResetResult resetRuntimeStateKeepingSpaces(@Nonnull String worldName) { - return callOwner("reset physics runtime state", - () -> resetRuntimeStateKeepingSpacesDirect(worldName)); - } - - @Nonnull - private PhysicsRuntimeResetResult resetRuntimeStateKeepingSpacesDirect(@Nonnull String worldName) { - PhysicsRuntimeResetResult reset = spaceRuntime.resetKeepingSpaces(worldName, - simulationRuntime.getWorldSettings().getStepMode()); - collisionRuntime.clearAll(); - clearRuntimeTopologyDirect(false); - markWorldChanged(); - return reset; - } - - @Nonnull - @Override - public PhysicsSpaceSettings getSpaceSettings(@Nonnull SpaceId spaceId) { - return spaceRuntime.getSpaceSettings(spaceId); - } - - @Nonnull - public PhysicsSpaceSettings getLiveSpaceSettings(@Nonnull SpaceId spaceId) { - return spaceRuntime.getLiveSpaceSettings(spaceId); - } - - @Override - public void setSpaceSettings(@Nonnull SpaceId spaceId, @Nonnull PhysicsSpaceSettings settings) { - PhysicsSpaceSettings requested = new PhysicsSpaceSettings(settings); - runOwnerMutation("set physics space settings", () -> setSpaceSettingsDirect(spaceId, requested)); - } - - @Nonnull - @Override - public PhysicsMutationHandle setSpaceSettingsAsync(@Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceSettings settings) { - PhysicsSpaceSettings requested = new PhysicsSpaceSettings(settings); - return enqueueOwnerMutation("set physics space settings", - spaceId, - () -> setSpaceSettingsDirect(spaceId, requested)); - } - - private void setSpaceSettingsDirect(@Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceSettings settings) { - PhysicsWorldCollisionSettings previousCollisionSettings = - spaceRuntime.getLiveSpaceSettings(spaceId).getWorldCollisionSettings(); - boolean worldCollisionSettingsChanged = - worldCollisionStreamingSettingsChanged(previousCollisionSettings, - settings.getWorldCollisionSettings()); - boolean terrainRepresentationChanged = - previousCollisionSettings.isNativeVoxelTerrainEnabled() - != settings.getWorldCollisionSettings().isNativeVoxelTerrainEnabled(); - boolean terrainMaterialChanged = - Float.compare(previousCollisionSettings.getTerrainFriction(), - settings.getWorldCollisionSettings().getTerrainFriction()) != 0 - || Float.compare(previousCollisionSettings.getTerrainRestitution(), - settings.getWorldCollisionSettings().getTerrainRestitution()) != 0; - boolean worldCollisionDisabled = - settings.getWorldCollisionSettings().getWorldCollisionMode() == WorldCollisionMode.NONE - && previousCollisionSettings.getWorldCollisionMode() != WorldCollisionMode.NONE; - spaceRuntime.setSpaceSettings(spaceId, settings); - if (worldCollisionDisabled || terrainRepresentationChanged || terrainMaterialChanged) { - collisionRuntime.clear(requireSpaceBinding(spaceId)); - } else if (worldCollisionSettingsChanged) { - collisionRuntime.incrementStreamingRevision(spaceId); - } - } - - private static boolean worldCollisionStreamingSettingsChanged( - @Nonnull PhysicsWorldCollisionSettings previous, - @Nonnull PhysicsWorldCollisionSettings next) { - return previous.getWorldCollisionMode() != next.getWorldCollisionMode() - || previous.getWorldCollisionRadius() != next.getWorldCollisionRadius() - || previous.getWorldCollisionBodyRadius() != next.getWorldCollisionBodyRadius() - || previous.getWorldCollisionTtlTicks() != next.getWorldCollisionTtlTicks() - || previous.isNativeVoxelTerrainEnabled() != next.isNativeVoxelTerrainEnabled(); - } - - private void validateStepModeSupported(@Nonnull PhysicsStepMode stepMode) { - spaceRuntime.validateStepModeSupported(stepMode); - } - - @Nonnull - public RigidBodyKey addBodyOnOwner(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull BackendBodyHandle backendBodyHandle, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - assertCanAccessLiveBackendDirectly("add physics body"); - return addBodyDirect(bodyKey, spaceId, backendBodyHandle, kind, persistenceMode); - } - - @Nonnull - private RigidBodyKey addBodyDirect(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull BackendBodyHandle backendBodyHandle, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - return bodyRuntime.addBody(bodyKey, spaceId, backendBodyHandle, kind, persistenceMode); - } - - @Override - public void destroyBody(@Nonnull RigidBodyKey bodyKey) { - destroyBody(bodyKey, true); - } - - @Nonnull - @Override - public PhysicsMutationHandle destroyBodyAsync(@Nonnull RigidBodyKey bodyKey) { - return destroyBodyAsync(bodyKey, true); - } - - public void destroyBody(@Nonnull RigidBodyKey bodyKey, boolean removeFromSpace) { - runOwnerMutation("destroy physics body", () -> destroyBodyDirect(bodyKey, removeFromSpace)); - } - - @Nonnull - public PhysicsMutationHandle destroyBodyAsync(@Nonnull RigidBodyKey bodyKey, - boolean removeFromSpace) { - return enqueueOwnerMutation("destroy physics body", - bodyKey, - () -> destroyBodyDirect(bodyKey, removeFromSpace)); - } - - private void destroyBodyDirect(@Nonnull RigidBodyKey bodyKey, boolean removeFromSpace) { - lifecycleState.clearBodyCreationPending(bodyKey); - bodyRuntime.destroyBody(bodyKey, removeFromSpace); - } - - @Nullable - public RigidBodyKey getBodyKey(@Nonnull SpaceId spaceId, long backendBodyId) { - return bodyRegistry.getBodyKey(spaceId, backendBodyId); - } - - @Nullable - public PhysicsBodyRegistration getBodyRegistration(@Nonnull SpaceId spaceId, long backendBodyId) { - assertCanAccessLiveBackendDirectly("resolve physics body registration"); - RigidBodyKey bodyKey = bodyRegistry.getBodyKey(spaceId, backendBodyId); - return bodyKey != null ? bodyRegistry.getRegistration(bodyKey) : null; - } - - @Nullable - public PhysicsBodyRegistration getRegistration(@Nonnull RigidBodyKey bodyKey) { - assertCanAccessLiveBackendDirectly("resolve physics body registration"); - return bodyRegistry.getRegistration(bodyKey); - } - - @Nullable - @Override - public PhysicsBodyRegistrationView getBodyRegistrationView(@Nonnull RigidBodyKey bodyKey) { - return bodyRegistry.getPublishedRegistrationView(bodyKey); - } - - @Nonnull - public JointKey addJointOnOwner(@Nonnull JointKey jointKey, - @Nonnull SpaceId spaceId, - @Nonnull BackendJointHandle backendJointHandle, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB, - @Nonnull JointType type, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ, - float restLength, - float stiffness, - float damping, - float lowerLimit, - float upperLimit, - boolean motorEnabled, - float motorTargetVelocity, - float motorMaxForce) { - assertCanAccessLiveBackendDirectly("add physics joint"); - return addJointDirect(jointKey, - spaceId, - backendJointHandle, - bodyA, - bodyB, - type, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - axisX, - axisY, - axisZ, - restLength, - stiffness, - damping, - lowerLimit, - upperLimit, - motorEnabled, - motorTargetVelocity, - motorMaxForce); - } - - @Nonnull - private JointKey addJointDirect(@Nonnull JointKey jointKey, - @Nonnull SpaceId spaceId, - @Nonnull BackendJointHandle backendJointHandle, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB, - @Nonnull JointType type, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ, - float restLength, - float stiffness, - float damping, - float lowerLimit, - float upperLimit, - boolean motorEnabled, - float motorTargetVelocity, - float motorMaxForce) { - if (spaceRuntime.getBinding(spaceId) == null) { - throw new IllegalArgumentException("Physics space id=" + spaceId + " is not registered"); - } - jointRegistry.registerJoint(jointKey, - spaceId, - backendJointHandle, - bodyA, - bodyB, - type, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - axisX, - axisY, - axisZ, - restLength, - stiffness, - damping, - lowerLimit, - upperLimit, - motorEnabled, - motorTargetVelocity, - motorMaxForce); - markWorldChanged(); - return jointKey; - } - - public boolean removeJoint(@Nonnull JointKey jointKey) { - return callOwner("remove physics joint", () -> removeJointDirect(jointKey)); - } - - private boolean removeJointDirect(@Nonnull JointKey jointKey) { - PhysicsJointRegistration registration = jointRegistry.getRegistration(jointKey); - if (registration == null) { - return false; - } - - PhysicsSpaceBinding binding = spaceRuntime.getBinding(registration.spaceId()); - if (binding != null) { - binding.runtime().removeJoint(binding.backendSpaceHandle().value(), registration.backendJointHandle().value()); - } - jointRegistry.unregisterJoint(jointKey); - markWorldChanged(); - return true; - } - - @Nullable - public JointKey getJointKey(@Nonnull SpaceId spaceId, long backendJointId) { - assertCanAccessLiveBackendDirectly("resolve physics joint key"); - return jointRegistry.getJointKey(spaceId, backendJointId); - } - - @Nullable - public PhysicsJointRegistration getJointRegistration(@Nonnull JointKey jointKey) { - assertCanAccessLiveBackendDirectly("resolve physics joint registration"); - return jointRegistry.getRegistration(jointKey); - } - - @Nonnull - public Collection getJointRegistrations() { - assertCanAccessLiveBackendDirectly("list physics joint registrations"); - return jointRegistry.getRegistrations(); - } - - @Nullable - public PhysicsJointRegistration findJointBetween(@Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB) { - assertCanAccessLiveBackendDirectly("resolve physics joint registration"); - return jointRegistry.findJointBetween(spaceId, bodyA, bodyB); - } - - public boolean isBodyCreationPending(@Nonnull RigidBodyKey bodyKey) { - return lifecycleState.isBodyCreationPending(bodyKey, - bodyRuntime.isBodyCreationPending(bodyKey), - ownerGateway.hasOwnerExecutor()); - } - - public boolean hasPublishedOrPendingBodyRegistration(@Nonnull RigidBodyKey bodyKey) { - return getBodyRegistrationView(bodyKey) != null - || isBodyCreationPending(bodyKey); - } - - @Nonnull - public PhysicsBodyRegistration requireBodyRegistration(@Nonnull RigidBodyKey bodyKey) { - PhysicsBodyRegistration registration = getRegistration(bodyKey); - if (registration == null) { - throw new IllegalArgumentException("Physics body key=" + bodyKey + " is not registered"); - } - return registration; - } - - @Nonnull - public Collection getBodyRegistrations() { - assertCanAccessLiveBackendDirectly("list physics body registrations"); - return bodyRegistry.getRegistrations(); - } - - @Nonnull - @Override - public Collection getBodyRegistrationViews() { - return bodyRegistry.getPublishedRegistrationViews(); - } - - @Override - public int getBodyRegistrationCount() { - return bodyRegistry.getPublishedRegistrationCount(); - } - - @Override - public int getBodyRegistrationCount(@Nonnull PhysicsBodyPersistenceMode persistenceMode) { - return bodyRegistry.getPublishedRegistrationCount(persistenceMode); - } - - @Nonnull - public Collection getBodyRegistrations(@Nonnull PhysicsBodyKind kind) { - assertCanAccessLiveBackendDirectly("list physics body registrations"); - return bodyRegistry.getRegistrations(kind); - } - - @Nonnull - @Override - public Collection getBodyRegistrationViews(@Nonnull PhysicsBodyKind kind) { - return bodyRegistry.getPublishedRegistrationViews(kind); - } - - @Nonnull - @Override - public Collection> getBodyAttachments(@Nonnull RigidBodyKey bodyKey) { - return visualRuntime.getAttachments(bodyKey); - } - - @Override - public boolean hasBodyAttachments(@Nonnull RigidBodyKey bodyKey) { - return visualRuntime.hasAttachments(bodyKey); - } - - public void registerBodyAttachment(@Nonnull RigidBodyKey bodyKey, @Nonnull Ref attachment) { - visualRuntime.registerAttachment(bodyKey, attachment); - } - - public void unregisterBodyAttachment(@Nonnull RigidBodyKey bodyKey, @Nonnull Ref attachment) { - visualRuntime.unregisterAttachment(bodyKey, attachment); - } - - @Nullable - public Ref getGeneratedVisualProxy(@Nonnull RigidBodyKey bodyKey) { - return visualRuntime.getGeneratedVisualProxy(bodyKey); - } - - @Nonnull - public Collection getGeneratedVisualProxyBodyKeys() { - return visualRuntime.getGeneratedVisualProxyBodyKeys(); - } - - public int getGeneratedVisualProxyCount() { - return visualRuntime.generatedVisualProxyCount(); - } - - public void setGeneratedVisualProxy(@Nonnull RigidBodyKey bodyKey, @Nonnull Ref proxy) { - visualRuntime.setGeneratedVisualProxy(bodyKey, proxy); - } - - public void clearGeneratedVisualProxy(@Nonnull RigidBodyKey bodyKey) { - visualRuntime.clearGeneratedVisualProxy(bodyKey); - } - - public boolean clearGeneratedVisualProxy(@Nonnull RigidBodyKey bodyKey, - @Nonnull Ref expectedProxy) { - return visualRuntime.clearGeneratedVisualProxy(bodyKey, expectedProxy); - } - - public boolean isGeneratedVisualProxy(@Nonnull RigidBodyKey bodyKey, - @Nonnull Ref proxy) { - return visualRuntime.isGeneratedVisualProxy(bodyKey, proxy); - } - - public void setSyntheticVisualInterests(@Nonnull Collection interests) { - visualRuntime.setSyntheticVisualInterests(interests); - } - - @Nonnull - public List getSyntheticVisualInterests() { - return visualRuntime.getSyntheticVisualInterests(); - } - - public void clearSyntheticVisualInterests() { - visualRuntime.clearSyntheticVisualInterests(); - } - - @Override - public void clearBodies() { - runOwnerMutation("clear physics bodies", this::destroyRegisteredBodiesDirect); - } - - @Nonnull - @Override - public PhysicsMutationHandle clearBodiesAsync() { - return enqueueOwnerMutation("clear physics bodies", this::destroyRegisteredBodiesDirect); - } - - private void destroyRegisteredBodiesDirect() { - bodyRuntime.destroyRegisteredBodies(); - } - - private void clearBodyStateDirect() { - clearRuntimeTopologyDirect(false); - markWorldChanged(); - } - - @Nonnull - public BodySyncState getOrCreateBodySyncState(@Nonnull Ref entityRef) { - return runtimeState.getOrCreateBodySyncState(entityRef); - } - - @Nullable - public BodySyncState getBodySyncState(@Nonnull Ref entityRef) { - return runtimeState.getBodySyncState(entityRef); - } - - public void clearBodySyncState(@Nonnull Ref entityRef) { - runtimeState.clearBodySyncState(entityRef); - } - - @Nonnull - public BodyVisualInterestState getOrCreateBodyVisualInterestState(@Nonnull RigidBodyKey bodyKey) { - BodyVisualInterestState state = visualRuntime.getOrCreateBodyVisualInterestState(bodyKey); - state.advanceVisualInterestTick(visualInterestTick.get()); - return state; - } - - @Nullable - public BodyVisualInterestState getBodyVisualInterestState(@Nonnull RigidBodyKey bodyKey) { - BodyVisualInterestState state = visualRuntime.getBodyVisualInterestState(bodyKey); - if (state != null) { - state.advanceVisualInterestTick(visualInterestTick.get()); - } - return state; - } - - public long advanceVisualInterestTick() { - return visualInterestTick.incrementAndGet(); - } - - public void markBodyControlled(@Nonnull RigidBodyKey bodyKey) { - controlRuntime.markBodyControlled(bodyKey); - } - - public void clearControlledBody(@Nonnull RigidBodyKey bodyKey) { - controlRuntime.clearControlledBody(bodyKey); - } - - public boolean isBodyControlled(@Nonnull RigidBodyKey bodyKey) { - return controlRuntime.isBodyControlled(bodyKey); - } - - public void disableControlLifecycle() { - controlRuntime.clear(); - } - - public void updateChunkBoundarySafeState(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f position, - @Nonnull Quaternionf rotation) { - chunkRuntime.updateChunkBoundarySafeState(bodyKey, position, rotation); - } - - public void updateChunkBoundarySafeState(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodySnapshot snapshot) { - chunkRuntime.updateChunkBoundarySafeState(bodyKey, snapshot); - } - - @Nullable - public ChunkBoundarySafeState getChunkBoundarySafeState(@Nonnull RigidBodyKey bodyKey) { - return chunkRuntime.getChunkBoundarySafeState(bodyKey); - } - - public void pauseChunkBoundaryBody(@Nonnull RigidBodyKey bodyKey, - long targetChunkIndex, - @Nonnull PhysicsBodyType originalBodyType, - @Nonnull Vector3f linearVelocity, - @Nonnull Vector3f angularVelocity) { - chunkRuntime.pauseChunkBoundaryBody(bodyKey, - targetChunkIndex, - originalBodyType, - linearVelocity, - angularVelocity); - } - - public void pauseChunkBoundaryBody(@Nonnull RigidBodyKey bodyKey, - long targetChunkIndex, - @Nonnull PhysicsBodySnapshot snapshot) { - chunkRuntime.pauseChunkBoundaryBody(bodyKey, targetChunkIndex, snapshot); - } - - public void pauseChunkBoundaryBody(@Nonnull RigidBodyKey bodyKey, - long targetChunkIndex, - @Nonnull long[] targetChunkIndices, - @Nonnull PhysicsBodySnapshot snapshot) { - chunkRuntime.pauseChunkBoundaryBody(bodyKey, targetChunkIndex, targetChunkIndices, snapshot); - } - - @Nullable - public ChunkBoundaryPauseState getChunkBoundaryPauseState(@Nonnull RigidBodyKey bodyKey) { - return chunkRuntime.getChunkBoundaryPauseState(bodyKey); - } - - public void clearChunkBoundaryPauseState(@Nonnull RigidBodyKey bodyKey) { - chunkRuntime.clearChunkBoundaryPauseState(bodyKey); - } - - public void clearBodyRuntimeState(@Nonnull RigidBodyKey bodyKey) { - runOwnerMutation("clear physics body runtime state", () -> clearBodyRuntimeStateDirect(bodyKey)); - } - - @Nonnull - public PhysicsMutationHandle clearBodyRuntimeStateAsync( - @Nonnull RigidBodyKey bodyKey) { - return enqueueOwnerMutation("clear physics body runtime state", - bodyKey, - () -> clearBodyRuntimeStateDirect(bodyKey)); - } - - private void clearBodyRuntimeStateDirect(@Nonnull RigidBodyKey bodyKey) { - bodyRuntime.clearBodyRuntimeState(bodyKey); - } - - public void markContinuousCollisionForced(@Nonnull RigidBodyKey bodyKey) { - chunkRuntime.markContinuousCollisionForced(bodyKey); - } - - @Nonnull - public Collection getForcedContinuousCollisionBodyKeys() { - return chunkRuntime.getForcedContinuousCollisionBodyKeys(); - } - - public boolean hasForcedContinuousCollisionBodies() { - return chunkRuntime.hasForcedContinuousCollisionBodies(); - } - - public void forEachForcedContinuousCollisionBody(@Nonnull Consumer consumer) { - chunkRuntime.forEachForcedContinuousCollisionBody(consumer); - } - - public void clearForcedContinuousCollisionBodies() { - chunkRuntime.clearForcedContinuousCollisionBodies(); - } - - public void copyFrom(@Nonnull PhysicsWorldResource other) { - runOwnerMutation("copy physics world resource", () -> copyFromDirect(other)); - } - - @Nonnull - public PhysicsMutationHandle copyFromAsync(@Nonnull PhysicsWorldResource other) { - return enqueueOwnerMutation("copy physics world resource", () -> copyFromDirect(other)); - } - - private void copyFromDirect(@Nonnull PhysicsWorldResource other) { - if (this == other) { - return; - } - PhysicsWorldRuntimeResource otherRuntime = require(other); - spaceRuntime.clearLiveTopology(""); - clearRuntimeTopologyDirect(true); - simulationRuntime.copyFrom(otherRuntime.simulationRuntime); - markWorldChanged(); - } - - private void clearRuntimeTopologyDirect(boolean clearCollision) { - bodyRuntime.clearBodyStateWithoutMarkingWorldChanged(); - if (clearCollision) { - collisionRuntime.clearAllAndUnregisterSpaces(); - } - } - - private void markBodyCreationPending(@Nonnull RigidBodyKey bodyKey) { - bodyRuntime.markBodyCreationPending(bodyKey); - } - - private void clearBodyCreationPending(@Nonnull RigidBodyKey bodyKey) { - bodyRuntime.clearBodyCreationPending(bodyKey); - } - - private boolean trackCommandBodyCreationPublication(@Nonnull RecordedPhysicsCommandBatch batch) { - return lifecycleState.trackBodyCreationPublication(batch, ownerGateway.hasOwnerExecutor()); - } - - private void clearCommandBodyCreationPublication(@Nonnull RecordedPhysicsCommandBatch batch) { - lifecycleState.clearBodyCreationPublication(batch); - } - - private void markWorldChanged() { - lifecycleState.markWorldChanged(bodyRegistry, ownerGateway.hasOwnerExecutor()); - } - - @Nonnull - private PhysicsCommandCompletion executeCommandBatch(@Nonnull RecordedPhysicsCommandBatch batch) { - return lifecycleState.executeCommandBatch(batch, simulationExecutor::execute); - } - - @Nonnull - @Override - public PhysicsWorldResource clone() { - PhysicsWorldRuntimeResource copy = new PhysicsWorldRuntimeResource(); - copy.copyFrom(this); - return copy; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldSettingsResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldSettingsResource.java new file mode 100644 index 00000000..1ae0b2e2 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldSettingsResource.java @@ -0,0 +1,47 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; +import lombok.Getter; +import lombok.Setter; +import javax.annotation.Nonnull; + +/** + * World-level PhysicsStore step settings. + */ +public final class PhysicsWorldSettingsResource implements Resource { + + @Nonnull + private final PhysicsWorldSettings settings = new PhysicsWorldSettings(); + @Setter + @Getter + private boolean ccdStepModeActive; + + public PhysicsWorldSettingsResource() { + } + + @Nonnull + public PhysicsWorldSettings getSettings() { + return new PhysicsWorldSettings(settings); + } + + public void setSettings(@Nonnull PhysicsWorldSettings settings) { + this.settings.copyFrom(settings); + } + + @Nonnull + @Override + public PhysicsWorldSettingsResource clone() { + PhysicsWorldSettingsResource copy = new PhysicsWorldSettingsResource(); + copy.setSettings(settings); + copy.ccdStepModeActive = ccdStepModeActive; + return copy; + } + + @Nonnull + public static ResourceType getResourceType() { + return PhysicsResourceTypes.worldSettingsResourceType(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldSnapshotState.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldSnapshotState.java deleted file mode 100644 index 38f555db..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldSnapshotState.java +++ /dev/null @@ -1,224 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources; - -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistry; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodySnapshotVisitor; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodySnapshotStore; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistration; -import dev.hytalemodding.impulse.core.plugin.snapshot.PhysicsBodySnapshotEntry; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import java.util.Collection; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import lombok.Getter; -import org.joml.Vector3f; - -/** - * Snapshot and epoch state for a world physics resource. - * - *

The owner-side store is used while capturing immutable frames on the physics owner. The - * reader-side store is only updated when a frame is applied for the current world epoch. Keeping - * both stores and the epoch counters together prevents stale owner frames from repopulating - * world-thread snapshots after topology changes.

- */ -public final class PhysicsWorldSnapshotState { - - private final PhysicsBodySnapshotStore bodySnapshots = new PhysicsBodySnapshotStore(); - private final PhysicsBodySnapshotStore ownerBodySnapshots = new PhysicsBodySnapshotStore(); - private final AtomicLong worldEpoch = new AtomicLong(); - private final AtomicLong snapshotFrameEpoch = new AtomicLong(); - @Nonnull - private final AtomicReference latestPublishedFrame = - new AtomicReference<>(PublishedPhysicsSnapshotFrame.empty(0L, 0L)); - @Getter - private volatile long latestSnapshotAppliedNanos; - - @Nullable - public PhysicsBodySnapshot getBodySnapshot(@Nonnull RigidBodyKey bodyKey) { - return bodySnapshots.get(bodyKey); - } - - @Nonnull - public PhysicsBodySnapshot captureBodySnapshot( - @Nonnull PhysicsBodyRegistration registration) { - PhysicsBodySnapshot snapshot = bodySnapshots.get(registration.bodyKey()); - if (snapshot == null) { - throw new IllegalStateException("No physics body snapshot is available for " + registration.bodyKey()); - } - return snapshot; - } - - public void putBodySnapshot(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - bodySnapshots.put(bodyKey, snapshot, spaceId, kind, persistenceMode); - ownerBodySnapshots.put(bodyKey, snapshot, spaceId, kind, persistenceMode); - } - - @Nonnull - public PublishedPhysicsSnapshotFrame capturePublishedSnapshotFrame( - @Nonnull Collection spaces, - @Nonnull PhysicsBodyRegistry bodyRegistry, - long stepSequence, - long serverTick, - long lastIncludedCommandBatchSequence, - @Nonnull PublishedPhysicsSnapshotFrame.Status status, - long stepNanos, - boolean profilingEnabled) { - Objects.requireNonNull(spaces, "spaces"); - Objects.requireNonNull(bodyRegistry, "bodyRegistry"); - Objects.requireNonNull(status, "status"); - - long frameEpoch = snapshotFrameEpoch.incrementAndGet(); - long frameWorldEpoch = worldEpoch.get(); - long snapshotStartNanos = profilingEnabled ? System.nanoTime() : 0L; - ownerBodySnapshots.refresh(spaces, bodyRegistry); - int spatialIndexCellCount = ownerBodySnapshots.cellCount(); - - int bodyCount = 0; - for (PhysicsSpaceBinding space : spaces) { - bodyCount += ownerBodySnapshots.bodyCount(space.spaceId()); - } - - long snapshotNanos = profilingEnabled ? System.nanoTime() - snapshotStartNanos : 0L; - PublishedPhysicsSnapshotFrame.Builder frameBuilder = PublishedPhysicsSnapshotFrame.compactBuilder(frameEpoch, - frameWorldEpoch, - stepSequence, - serverTick, - lastIncludedCommandBatchSequence, - status, - spatialIndexCellCount, - stepNanos, - snapshotNanos, - spaces.size(), - bodyCount); - for (PhysicsSpaceBinding space : spaces) { - SpaceId spaceId = space.spaceId(); - int spaceBodyCount = ownerBodySnapshots.bodyCount(spaceId); - frameBuilder.addSpace(spaceId, frameWorldEpoch, spaceBodyCount); - ownerBodySnapshots.forEachIndexed(spaceId, - (bodyKey, snapshot, bodySpaceId, kind, persistenceMode) -> frameBuilder.addBody(bodyKey, - bodySpaceId, - frameWorldEpoch, - frameWorldEpoch, - kind, - persistenceMode, - snapshot)); - } - PublishedPhysicsSnapshotFrame frame = frameBuilder.build(); - publishLatestFrameIfWorldCurrent(frame); - return frame; - } - - @Nonnull - public ApplyResult applyPublishedSnapshotFrame(@Nonnull PublishedPhysicsSnapshotFrame frame) { - Objects.requireNonNull(frame, "frame"); - if (frame.worldEpoch() != worldEpoch.get()) { - return new ApplyResult(0, false); - } - - PhysicsBodySnapshotStore.ApplyStats stats = - bodySnapshots.applyPublishedFrame(frame); - latestSnapshotAppliedNanos = System.nanoTime(); - return new ApplyResult(stats.applied(), true); - } - - public record ApplyResult(int appliedCount, boolean currentWorldEpoch) { - } - - @Nonnull - public PublishedPhysicsSnapshotFrame getLatestPublishedFrame() { - return latestPublishedFrame.get(); - } - - public long worldEpoch() { - return worldEpoch.get(); - } - - public int getBodySnapshotCount() { - return bodySnapshots.bodyCount(); - } - - public int getBodySnapshotCount(@Nonnull SpaceId spaceId) { - return bodySnapshots.bodyCount(spaceId); - } - - public int getBodySnapshotCellCount() { - return bodySnapshots.cellCount(); - } - - public void forEachBodySnapshot(@Nonnull SpaceId spaceId, - @Nonnull Consumer consumer) { - bodySnapshots.forEach(spaceId, consumer); - } - - public void forEachIndexedBodySnapshot(@Nonnull SpaceId spaceId, - @Nonnull PhysicsBodySnapshotVisitor visitor) { - bodySnapshots.forEachIndexed(spaceId, visitor); - } - - public int forEachBodySnapshotNear(@Nonnull SpaceId spaceId, - @Nonnull Vector3f center, - float radius, - @Nonnull Consumer consumer) { - return bodySnapshots.forEachNear(spaceId, center, radius, consumer); - } - - public int forEachIndexedBodySnapshotNear(@Nonnull SpaceId spaceId, - @Nonnull Vector3f center, - float radius, - @Nonnull PhysicsBodySnapshotVisitor visitor) { - return bodySnapshots.forEachIndexedNear(spaceId, center, radius, visitor); - } - - public void removeBodySnapshot(@Nonnull RigidBodyKey bodyKey) { - bodySnapshots.remove(bodyKey); - ownerBodySnapshots.remove(bodyKey); - } - - public void clearBodySnapshots() { - bodySnapshots.clear(); - ownerBodySnapshots.clear(); - } - - public void markWorldChanged() { - long newWorldEpoch = worldEpoch.incrementAndGet(); - publishLatestFrame(PublishedPhysicsSnapshotFrame.empty(snapshotFrameEpoch.get(), newWorldEpoch)); - } - - private void publishLatestFrameIfWorldCurrent(@Nonnull PublishedPhysicsSnapshotFrame frame) { - publishLatestFrame(frame, true); - } - - private void publishLatestFrame(@Nonnull PublishedPhysicsSnapshotFrame frame) { - publishLatestFrame(frame, false); - } - - private void publishLatestFrame(@Nonnull PublishedPhysicsSnapshotFrame frame, - boolean requireCurrentWorldEpoch) { - latestPublishedFrame.updateAndGet(current -> { - if (requireCurrentWorldEpoch && frame.worldEpoch() != worldEpoch.get()) { - return current; - } - return isNewerFrame(frame, current) ? frame : current; - }); - } - - private static boolean isNewerFrame(@Nonnull PublishedPhysicsSnapshotFrame candidate, - @Nonnull PublishedPhysicsSnapshotFrame current) { - if (candidate.worldEpoch() != current.worldEpoch()) { - return candidate.worldEpoch() > current.worldEpoch(); - } - return candidate.frameEpoch() >= current.frameEpoch(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyCreationTracker.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyCreationTracker.java deleted file mode 100644 index 69a24a31..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyCreationTracker.java +++ /dev/null @@ -1,47 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.body; - -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; -import javax.annotation.Nonnull; - -/** - * Tracks body keys reserved by async body registrations until publication catches up. - */ -public final class PhysicsBodyCreationTracker { - - private final Object2IntOpenHashMap pendingBodyCreations = - new Object2IntOpenHashMap<>(); - - public void markPending(@Nonnull RigidBodyKey bodyKey) { - synchronized (pendingBodyCreations) { - pendingBodyCreations.addTo(bodyKey, 1); - } - } - - public void clearPending(@Nonnull RigidBodyKey bodyKey) { - synchronized (pendingBodyCreations) { - clearPendingDirect(bodyKey); - } - } - - public boolean isPending(@Nonnull RigidBodyKey bodyKey) { - synchronized (pendingBodyCreations) { - return pendingBodyCreations.containsKey(bodyKey); - } - } - - public void clear() { - synchronized (pendingBodyCreations) { - pendingBodyCreations.clear(); - } - } - - private void clearPendingDirect(RigidBodyKey bodyKey) { - int count = pendingBodyCreations.getInt(bodyKey); - if (count <= 1) { - pendingBodyCreations.removeInt(bodyKey); - } else { - pendingBodyCreations.put(bodyKey, count - 1); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyRegistration.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyRegistration.java deleted file mode 100644 index 576e1a68..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyRegistration.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.body; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import javax.annotation.Nonnull; - -/** - * Owner-lane registration for a stable body key and backend-local body handle. - */ -public record PhysicsBodyRegistration(@Nonnull RigidBodyKey bodyKey, - @Nonnull BackendBodyHandle backendBodyHandle, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyRegistry.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyRegistry.java deleted file mode 100644 index ccd1a6bd..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyRegistry.java +++ /dev/null @@ -1,369 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.body; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyRegistrationView; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsBodySnapshotCursor; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.ObjectArrayList; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.function.Consumer; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Runtime identity index for backend physics bodies. - */ -public final class PhysicsBodyRegistry { - - private final Map registrationsByKey = - new Object2ObjectLinkedOpenHashMap<>(); - private final Map registrationViewsByKey = - new Object2ObjectOpenHashMap<>(); - private final Map publishedRegistrationViewsByKey = - new Object2ObjectLinkedOpenHashMap<>(); - private final Object2LongOpenHashMap publishedLivenessMarks = - new Object2LongOpenHashMap<>(); - private final Int2ObjectOpenHashMap> bodyKeysByRawBackendId = - new Int2ObjectOpenHashMap<>(); - private final Int2ObjectOpenHashMap> registrationsBySpace = - new Int2ObjectOpenHashMap<>(); - private long publishedLivenessGeneration; - - @Nonnull - public PhysicsBodyRegistration registerBody(@Nonnull RigidBodyKey bodyKey, - @Nonnull BackendBodyHandle backendBodyHandle, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - validateRegisterable(bodyKey, backendBodyHandle, spaceId); - PhysicsBodyRegistration existingRegistration = registrationsByKey.get(bodyKey); - if (existingRegistration != null) { - removeFromSpace(existingRegistration); - removeBackendIndex(existingRegistration); - } - PhysicsBodyRegistration registration = - new PhysicsBodyRegistration(bodyKey, backendBodyHandle, spaceId, kind, persistenceMode); - registrationsByKey.put(bodyKey, registration); - registrationViewsByKey.put(bodyKey, - new PhysicsBodyRegistrationView(bodyKey, spaceId, kind, persistenceMode)); - bodyKeysByRawBackendId - .computeIfAbsent(spaceId.value(), ignored -> new Long2ObjectOpenHashMap<>()) - .put(backendBodyHandle.value(), bodyKey); - addToSpace(registration); - return registration; - } - - public void validateRegisterable(@Nonnull RigidBodyKey bodyKey, - @Nonnull BackendBodyHandle backendBodyHandle, - @Nonnull SpaceId spaceId) { - Long2ObjectOpenHashMap bodyKeys = - bodyKeysByRawBackendId.get(spaceId.value()); - RigidBodyKey existingKey = bodyKeys != null ? bodyKeys.get(backendBodyHandle.value()) : null; - if (existingKey != null && !existingKey.equals(bodyKey)) { - throw new IllegalArgumentException("Physics body is already registered as " + existingKey); - } - PhysicsBodyRegistration existingRegistration = registrationsByKey.get(bodyKey); - if (existingRegistration != null - && (!existingRegistration.backendBodyHandle().equals(backendBodyHandle) - || !existingRegistration.spaceId().equals(spaceId))) { - throw new IllegalArgumentException("Physics body key=" + bodyKey - + " is already registered to another backend body"); - } - } - - @Nullable - public PhysicsBodyRegistration unregisterBody(@Nonnull RigidBodyKey bodyKey) { - PhysicsBodyRegistration registration = registrationsByKey.remove(bodyKey); - if (registration == null) { - return null; - } - - registrationViewsByKey.remove(bodyKey); - removeBackendIndex(registration); - removeFromSpace(registration); - return registration; - } - - @Nullable - public PhysicsBodyRegistration unregisterBody(@Nonnull SpaceId spaceId, long backendBodyId) { - RigidBodyKey bodyKey = getBodyKey(spaceId, backendBodyId); - return bodyKey != null ? unregisterBody(bodyKey) : null; - } - - @Nullable - public PhysicsBodyRegistration getRegistration(@Nonnull RigidBodyKey bodyKey) { - return registrationsByKey.get(bodyKey); - } - - @Nullable - public PhysicsBodyRegistrationView getRegistrationView(@Nonnull RigidBodyKey bodyKey) { - return registrationViewsByKey.get(bodyKey); - } - - @Nullable - public PhysicsBodyRegistrationView getPublishedRegistrationView(@Nonnull RigidBodyKey bodyKey) { - return publishedRegistrationViewsByKey.get(bodyKey); - } - - @Nonnull - public Collection getRegistrationViews() { - List views = new ArrayList<>(); - for (PhysicsBodyRegistration registration : registrationsByKey.values()) { - views.add(registrationViewsByKey.get(registration.bodyKey())); - } - return views; - } - - @Nonnull - public Collection getPublishedRegistrationViews() { - return new ArrayList<>(publishedRegistrationViewsByKey.values()); - } - - @Nonnull - public Collection getPublishedRegistrationViews(@Nonnull PhysicsBodyKind kind) { - List views = new ArrayList<>(); - for (PhysicsBodyRegistrationView view : publishedRegistrationViewsByKey.values()) { - if (view.kind() == kind) { - views.add(view); - } - } - return views; - } - - @Nonnull - public Collection getRegistrationViews(@Nonnull PhysicsBodyKind kind) { - List views = new ArrayList<>(); - for (PhysicsBodyRegistration registration : registrationsByKey.values()) { - if (registration.kind() == kind) { - views.add(registrationViewsByKey.get(registration.bodyKey())); - } - } - return views; - } - - @Nullable - public RigidBodyKey getBodyKey(@Nonnull SpaceId spaceId, long backendBodyId) { - Long2ObjectOpenHashMap bodyKeys = - bodyKeysByRawBackendId.get(spaceId.value()); - return bodyKeys != null ? bodyKeys.get(backendBodyId) : null; - } - - @Nullable - public RigidBodyKey getBodyKey(@Nonnull SpaceId spaceId, - @Nonnull BackendBodyHandle backendBodyHandle) { - return getBodyKey(spaceId, backendBodyHandle.value()); - } - - @Nonnull - public Collection getBodyKeys() { - return new ArrayList<>(registrationsByKey.keySet()); - } - - @Nonnull - public Collection getRegistrations() { - return new ArrayList<>(registrationsByKey.values()); - } - - public int getRegistrationCount() { - return registrationsByKey.size(); - } - - public int getPublishedRegistrationCount() { - return publishedRegistrationViewsByKey.size(); - } - - public void forEachRegistration(@Nonnull Consumer consumer) { - registrationsByKey.values().forEach(consumer); - } - - public void forEachRegistration(@Nonnull SpaceId spaceId, - @Nonnull Consumer consumer) { - ObjectArrayList registrations = - registrationsBySpace.get(spaceId.value()); - if (registrations == null) { - return; - } - registrations.forEach(consumer); - } - - @Nonnull - Iterator registrationIterator(@Nonnull SpaceId spaceId) { - ObjectArrayList registrations = - registrationsBySpace.get(spaceId.value()); - if (registrations == null) { - return Collections.emptyIterator(); - } - return new Iterator<>() { - - private int index; - - @Override - public boolean hasNext() { - return index < registrations.size(); - } - - @Override - public PhysicsBodyRegistration next() { - if (!hasNext()) { - throw new NoSuchElementException(); - } - return registrations.get(index++); - } - }; - } - - public int getRegistrationCount(@Nonnull SpaceId spaceId) { - ObjectArrayList registrations = - registrationsBySpace.get(spaceId.value()); - return registrations != null ? registrations.size() : 0; - } - - public int getRegistrationCount(@Nonnull PhysicsBodyPersistenceMode persistenceMode) { - int count = 0; - for (PhysicsBodyRegistration registration : registrationsByKey.values()) { - if (registration.persistenceMode() == persistenceMode) { - count++; - } - } - return count; - } - - public int getPublishedRegistrationCount(@Nonnull PhysicsBodyPersistenceMode persistenceMode) { - int count = 0; - for (PhysicsBodyRegistrationView view : publishedRegistrationViewsByKey.values()) { - if (view.persistenceMode() == persistenceMode) { - count++; - } - } - return count; - } - - @Nonnull - public Collection getRegistrations(@Nonnull PhysicsBodyKind kind) { - List registrations = new ArrayList<>(); - for (PhysicsBodyRegistration registration : registrationsByKey.values()) { - if (registration.kind() == kind) { - registrations.add(registration); - } - } - return registrations; - } - - public void clear() { - registrationsByKey.clear(); - registrationViewsByKey.clear(); - publishedRegistrationViewsByKey.clear(); - publishedLivenessMarks.clear(); - bodyKeysByRawBackendId.clear(); - registrationsBySpace.clear(); - } - - public void publishLiveRegistrationViews() { - long generation = nextPublishedLivenessGeneration(); - for (PhysicsBodyRegistration registration : registrationsByKey.values()) { - publishRegistrationView(registration.bodyKey(), - registration.spaceId(), - registration.kind(), - registration.persistenceMode(), - generation); - } - retainPublishedRegistrationViews(generation); - } - - public void applyPublishedRegistrationFrame(@Nonnull PublishedPhysicsSnapshotFrame frame) { - long generation = nextPublishedLivenessGeneration(); - frame.forEachBodyCursor(body -> publishRegistrationView(body, generation)); - retainPublishedRegistrationViews(generation); - } - - private void addToSpace(@Nonnull PhysicsBodyRegistration registration) { - registrationsBySpace - .computeIfAbsent(registration.spaceId().value(), ignored -> new ObjectArrayList<>()) - .add(registration); - } - - private void removeFromSpace(@Nonnull PhysicsBodyRegistration registration) { - ObjectArrayList registrations = - registrationsBySpace.get(registration.spaceId().value()); - if (registrations == null) { - return; - } - registrations.remove(registration); - if (registrations.isEmpty()) { - registrationsBySpace.remove(registration.spaceId().value()); - } - } - - private void removeBackendIndex(@Nonnull PhysicsBodyRegistration registration) { - Long2ObjectOpenHashMap bodyKeys = - bodyKeysByRawBackendId.get(registration.spaceId().value()); - if (bodyKeys == null) { - return; - } - bodyKeys.remove(registration.backendBodyHandle().value()); - if (bodyKeys.isEmpty()) { - bodyKeysByRawBackendId.remove(registration.spaceId().value()); - } - } - - private void publishRegistrationView(@Nonnull PublishedPhysicsBodySnapshotCursor body, - long generation) { - RigidBodyKey bodyKey = body.bodyKey(); - publishRegistrationView(bodyKey, - body.spaceId(), - body.kind(), - body.persistenceMode(), - generation); - } - - private void publishRegistrationView(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode, - long generation) { - PhysicsBodyRegistrationView existing = publishedRegistrationViewsByKey.get(bodyKey); - if (existing == null - || !existing.spaceId().equals(spaceId) - || existing.kind() != kind - || existing.persistenceMode() != persistenceMode) { - publishedRegistrationViewsByKey.put(bodyKey, - new PhysicsBodyRegistrationView(bodyKey, spaceId, kind, persistenceMode)); - } - publishedLivenessMarks.put(bodyKey, generation); - } - - private long nextPublishedLivenessGeneration() { - publishedLivenessGeneration++; - if (publishedLivenessGeneration == 0L) { - publishedLivenessGeneration = 1L; - publishedLivenessMarks.clear(); - } - return publishedLivenessGeneration; - } - - private void retainPublishedRegistrationViews(long generation) { - Iterator iterator = publishedRegistrationViewsByKey.keySet().iterator(); - while (iterator.hasNext()) { - RigidBodyKey bodyKey = iterator.next(); - if (publishedLivenessMarks.getLong(bodyKey) != generation) { - iterator.remove(); - publishedLivenessMarks.removeLong(bodyKey); - } - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyRuntime.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyRuntime.java deleted file mode 100644 index 8c336db7..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyRuntime.java +++ /dev/null @@ -1,182 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.body; - -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.modules.control.PhysicsControlRuntimeState; -import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.PhysicsChunkBoundaryRuntime; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceRuntime; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldLifecycleState; -import dev.hytalemodding.impulse.core.internal.resources.joint.PhysicsJointRegistry; -import dev.hytalemodding.impulse.core.internal.resources.joint.PhysicsJointRegistration; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import java.util.ArrayList; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Body lifecycle orchestration for one physics world. - */ -public final class PhysicsBodyRuntime { - - @Nonnull - private final PhysicsSpaceRuntime spaceRuntime; - @Nonnull - private final PhysicsBodyRegistry bodyRegistry; - @Nonnull - private final PhysicsBodyRuntimeState runtimeState; - @Nonnull - private final PhysicsControlRuntimeState controlRuntime; - @Nonnull - private final PhysicsJointRegistry jointRegistry; - @Nonnull - private final PhysicsChunkBoundaryRuntime chunkRuntime; - @Nonnull - private final PhysicsVisualRuntime visualRuntime; - @Nonnull - private final PhysicsWorldLifecycleState lifecycleState; - @Nonnull - private final Runnable worldChangedMarker; - @Nonnull - private final PhysicsBodyCreationTracker creationTracker = new PhysicsBodyCreationTracker(); - - public PhysicsBodyRuntime(@Nonnull PhysicsSpaceRuntime spaceRuntime, - @Nonnull PhysicsBodyRegistry bodyRegistry, - @Nonnull PhysicsBodyRuntimeState runtimeState, - @Nonnull PhysicsControlRuntimeState controlRuntime, - @Nonnull PhysicsJointRegistry jointRegistry, - @Nonnull PhysicsChunkBoundaryRuntime chunkRuntime, - @Nonnull PhysicsVisualRuntime visualRuntime, - @Nonnull PhysicsWorldLifecycleState lifecycleState, - @Nonnull Runnable worldChangedMarker) { - this.spaceRuntime = spaceRuntime; - this.bodyRegistry = bodyRegistry; - this.runtimeState = runtimeState; - this.controlRuntime = controlRuntime; - this.jointRegistry = jointRegistry; - this.chunkRuntime = chunkRuntime; - this.visualRuntime = visualRuntime; - this.lifecycleState = lifecycleState; - this.worldChangedMarker = worldChangedMarker; - } - - public void markBodyCreationPending(@Nonnull RigidBodyKey bodyKey) { - creationTracker.markPending(bodyKey); - } - - public void clearBodyCreationPending(@Nonnull RigidBodyKey bodyKey) { - creationTracker.clearPending(bodyKey); - } - - public boolean isBodyCreationPending(@Nonnull RigidBodyKey bodyKey) { - return creationTracker.isPending(bodyKey); - } - - @Nonnull - public RigidBodyKey addBody(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull BackendBodyHandle backendBodyHandle, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - PhysicsSpaceBinding binding = spaceRuntime.requireBinding(spaceId); - long backendBodyId = backendBodyHandle.value(); - if (!binding.runtime().containsBody(binding.backendSpaceHandle().value(), backendBodyId)) { - throw new IllegalArgumentException("Physics backend body id=" + backendBodyId - + " is not registered in space " + spaceId); - } - bodyRegistry.validateRegisterable(bodyKey, backendBodyHandle, spaceId); - PhysicsBodyRegistration registration = - bodyRegistry.registerBody(bodyKey, backendBodyHandle, spaceId, kind, persistenceMode); - PhysicsBodySnapshot snapshot = PhysicsBodySnapshots.read(binding, backendBodyId); - if (snapshot != null) { - lifecycleState.putBodySnapshot(bodyKey, - snapshot, - spaceId, - registration.kind(), - registration.persistenceMode()); - } - worldChangedMarker.run(); - return bodyKey; - } - - public void destroyBody(@Nonnull RigidBodyKey bodyKey, boolean removeFromSpace) { - PhysicsBodyRegistration registration = bodyRegistry.getRegistration(bodyKey); - if (registration != null) { - if (removeFromSpace) { - removeBodyFromSpace(registration); - } - bodyRegistry.unregisterBody(bodyKey); - clearBodyRuntimeState(bodyKey); - } else { - clearBodyRuntimeState(bodyKey); - } - worldChangedMarker.run(); - } - - public void destroyRegisteredBodies() { - RuntimeException failure = null; - boolean bodyFailure = false; - for (PhysicsBodyRegistration registration : new ArrayList<>(bodyRegistry.getRegistrations())) { - try { - destroyBody(registration.bodyKey(), true); - } catch (RuntimeException exception) { - bodyFailure = true; - failure = collectFailure(failure, exception); - } - } - if (bodyFailure) { - throw failure; - } - clearBodyState(); - } - - public void clearBodyState() { - clearBodyStateWithoutMarkingWorldChanged(); - worldChangedMarker.run(); - } - - public void clearBodyStateWithoutMarkingWorldChanged() { - bodyRegistry.clear(); - runtimeState.clear(); - controlRuntime.clear(); - jointRegistry.clear(); - chunkRuntime.clear(); - visualRuntime.clear(); - lifecycleState.clearBodySnapshots(); - creationTracker.clear(); - } - - public void clearBodyRuntimeState(@Nonnull RigidBodyKey bodyKey) { - visualRuntime.clearBodyRuntimeState(bodyKey); - controlRuntime.clearBody(bodyKey); - chunkRuntime.clearBody(bodyKey); - lifecycleState.removeBodySnapshot(bodyKey); - } - - private void removeBodyFromSpace(@Nonnull PhysicsBodyRegistration registration) { - for (PhysicsJointRegistration joint : jointRegistry.unregisterJointsForBody(registration.bodyKey())) { - PhysicsSpaceBinding jointSpace = spaceRuntime.getBinding(joint.spaceId()); - if (jointSpace != null) { - jointSpace.runtime().removeJoint(jointSpace.backendSpaceHandle().value(), joint.backendJointHandle().value()); - } - } - PhysicsSpaceBinding binding = spaceRuntime.getBinding(registration.spaceId()); - if (binding != null) { - binding.runtime().removeBody(binding.backendSpaceHandle().value(), registration.backendBodyHandle().value()); - } - } - - @Nonnull - private static RuntimeException collectFailure(@Nullable RuntimeException failure, - @Nonnull RuntimeException exception) { - if (failure == null) { - return exception; - } - failure.addSuppressed(exception); - return failure; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodySnapshotStore.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodySnapshotStore.java deleted file mode 100644 index b46bfb0d..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodySnapshotStore.java +++ /dev/null @@ -1,232 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.body; - -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.snapshot.PhysicsBodySnapshotEntry; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsBodySnapshotCursor; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import java.util.Iterator; -import java.util.Map; -import java.util.function.Consumer; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3f; - -/** - * Cached body snapshots and spatial lookup state for world-level readers. - */ -public final class PhysicsBodySnapshotStore { - - private final Map snapshots = - new Object2ObjectOpenHashMap<>(); - private final Object2LongOpenHashMap livenessMarks = - new Object2LongOpenHashMap<>(); - private final PhysicsBodySpatialIndex spatialIndex = new PhysicsBodySpatialIndex(); - private long livenessGeneration; - - public record ApplyStats(int applied, int inserted, int removed) { - } - - public int refresh(@Nonnull Iterable spaces, @Nonnull PhysicsBodyRegistry bodyRegistry) { - long generation = nextLivenessGeneration(); - MutableInt liveBodies = new MutableInt(); - for (PhysicsSpaceBinding space : spaces) { - SpaceId spaceId = space.spaceId(); - if (bodyRegistry.getRegistrationCount(spaceId) == 0) { - continue; - } - - Iterator registrations = bodyRegistry.registrationIterator(spaceId); - while (registrations.hasNext()) { - PhysicsBodyRegistration registration = registrations.next(); - PhysicsBodySnapshot snapshot = PhysicsBodySnapshots.read(space, registration.backendBodyHandle().value()); - if (snapshot == null) { - continue; - } - RigidBodyKey bodyKey = registration.bodyKey(); - markLive(bodyKey, generation, liveBodies); - PhysicsBodySnapshot previous = snapshots.get(bodyKey); - if (snapshot != previous) { - snapshots.put(bodyKey, snapshot); - } - spatialIndex.update(bodyKey, - snapshot, - spaceId, - registration.kind(), - registration.persistenceMode()); - } - } - - retainMarked(generation); - return liveBodies.value(); - } - - @Nonnull - public ApplyStats applyPublishedFrame(@Nonnull PublishedPhysicsSnapshotFrame frame) { - PublishedFrameApplier applier = new PublishedFrameApplier(nextLivenessGeneration()); - frame.forEachBodyCursor(applier); - return new ApplyStats(applier.applied(), applier.inserted(), retainMarked(applier.generation())); - } - - public void put(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - snapshots.put(bodyKey, snapshot); - livenessMarks.put(bodyKey, livenessGeneration); - spatialIndex.update(bodyKey, snapshot, spaceId, kind, persistenceMode); - } - - @Nullable - public PhysicsBodySnapshot get(@Nonnull RigidBodyKey bodyKey) { - return snapshots.get(bodyKey); - } - - public void remove(@Nonnull RigidBodyKey bodyKey) { - snapshots.remove(bodyKey); - livenessMarks.removeLong(bodyKey); - spatialIndex.remove(bodyKey); - } - - public void clear() { - snapshots.clear(); - livenessMarks.clear(); - spatialIndex.clear(); - } - - public int bodyCount() { - return spatialIndex.bodyCount(); - } - - public int bodyCount(@Nonnull SpaceId spaceId) { - return spatialIndex.bodyCount(spaceId); - } - - public int cellCount() { - return spatialIndex.cellCount(); - } - - public void forEach(@Nonnull SpaceId spaceId, - @Nonnull Consumer consumer) { - spatialIndex.forEach(spaceId, consumer); - } - - public void forEachIndexed(@Nonnull SpaceId spaceId, - @Nonnull PhysicsBodySnapshotVisitor visitor) { - spatialIndex.forEachIndexed(spaceId, visitor); - } - - public int forEachNear(@Nonnull SpaceId spaceId, - @Nonnull Vector3f center, - float radius, - @Nonnull Consumer consumer) { - return spatialIndex.forEachNear(spaceId, center, radius, consumer); - } - - public int forEachIndexedNear(@Nonnull SpaceId spaceId, - @Nonnull Vector3f center, - float radius, - @Nonnull PhysicsBodySnapshotVisitor visitor) { - return spatialIndex.forEachIndexedNear(spaceId, center, radius, visitor); - } - - private long nextLivenessGeneration() { - livenessGeneration++; - if (livenessGeneration == 0L) { - livenessGeneration = 1L; - livenessMarks.clear(); - } - return livenessGeneration; - } - - private void markLive(@Nonnull RigidBodyKey bodyKey, - long generation, - @Nonnull MutableInt liveBodies) { - if (livenessMarks.put(bodyKey, generation) != generation) { - liveBodies.increment(); - } - } - - private int retainMarked(long generation) { - int removed = 0; - Iterator iterator = snapshots.keySet().iterator(); - while (iterator.hasNext()) { - RigidBodyKey bodyKey = iterator.next(); - if (livenessMarks.getLong(bodyKey) != generation) { - iterator.remove(); - livenessMarks.removeLong(bodyKey); - spatialIndex.remove(bodyKey); - removed++; - } - } - return removed; - } - - private final class PublishedFrameApplier implements Consumer { - - private final long generation; - private final MutableInt liveBodies = new MutableInt(); - private int applied; - private int inserted; - - private PublishedFrameApplier(long generation) { - this.generation = generation; - } - - @Override - public void accept(@Nonnull PublishedPhysicsBodySnapshotCursor bodyFrame) { - RigidBodyKey bodyKey = bodyFrame.bodyKey(); - markLive(bodyKey, generation, liveBodies); - PhysicsBodySnapshot snapshot = snapshots.get(bodyKey); - if (snapshot == null) { - inserted++; - snapshot = bodyFrame.toBodySnapshot(); - snapshots.put(bodyKey, snapshot); - } else if (!bodyFrame.matchesSnapshot(snapshot)) { - snapshot = bodyFrame.toBodySnapshot(); - snapshots.put(bodyKey, snapshot); - } else { - applied++; - return; - } - spatialIndex.update(bodyKey, - snapshot, - bodyFrame.spaceId(), - bodyFrame.kind(), - bodyFrame.persistenceMode()); - applied++; - } - - private int applied() { - return applied; - } - - private int inserted() { - return inserted; - } - - private long generation() { - return generation; - } - } - - private static final class MutableInt { - - private int value; - - private void increment() { - value++; - } - - private int value() { - return value; - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodySnapshotVisitor.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodySnapshotVisitor.java deleted file mode 100644 index e6ebdab5..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodySnapshotVisitor.java +++ /dev/null @@ -1,21 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.body; - -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import javax.annotation.Nonnull; - -/** - * Internal snapshot visitor that avoids allocating public snapshot-entry DTOs. - */ -@FunctionalInterface -public interface PhysicsBodySnapshotVisitor { - - void accept(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode); -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodySnapshots.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodySnapshots.java deleted file mode 100644 index 103a3181..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodySnapshots.java +++ /dev/null @@ -1,189 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.body; - -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.runtime.BackendBodySnapshotSink; -import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; -import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Internal conversion helpers for primitive backend-runtime body snapshots. - */ -public final class PhysicsBodySnapshots { - - private PhysicsBodySnapshots() { - } - - @Nullable - public static PhysicsBodySnapshot read(@Nonnull PhysicsSpaceBinding space, - long backendBodyId) { - return read(space.runtime(), space.backendSpaceHandle().value(), backendBodyId); - } - - @Nullable - public static PhysicsBodySnapshot read(@Nonnull PhysicsBackendRuntime runtime, - int spaceId, - long backendBodyId) { - SnapshotCapture capture = new SnapshotCapture(backendBodyId); - boolean present = runtime.bodySnapshot(spaceId, - backendBodyId, - capture); - return present ? capture.snapshot : null; - } - - @Nonnull - public static PhysicsBodySnapshot fromRuntimeFields(int shapeTypeCode, - int bodyTypeCode, - float positionX, - float positionY, - float positionZ, - float rotationX, - float rotationY, - float rotationZ, - float rotationW, - float linearVelocityX, - float linearVelocityY, - float linearVelocityZ, - float angularVelocityX, - float angularVelocityY, - float angularVelocityZ, - boolean sleeping, - boolean sensor, - float mass, - float friction, - float restitution, - float linearDamping, - float angularDamping, - int collisionGroup, - int collisionMask, - boolean continuousCollisionEnabled, - float centerOfMassOffsetY, - boolean hasBoxHalfExtents, - float halfExtentX, - float halfExtentY, - float halfExtentZ, - float radius, - float halfHeight, - int axisCode) { - return PhysicsBodySnapshot.of(positionX, - positionY, - positionZ, - rotationX, - rotationY, - rotationZ, - rotationW, - linearVelocityX, - linearVelocityY, - linearVelocityZ, - angularVelocityX, - angularVelocityY, - angularVelocityZ, - BackendRuntimeCodes.bodyType(bodyTypeCode), - sleeping, - sensor, - mass, - friction, - restitution, - linearDamping, - angularDamping, - collisionGroup, - collisionMask, - continuousCollisionEnabled, - centerOfMassOffsetY, - BackendRuntimeCodes.shapeType(shapeTypeCode), - hasBoxHalfExtents, - halfExtentX, - halfExtentY, - halfExtentZ, - radius, - halfHeight, - BackendRuntimeCodes.axis(axisCode)); - } - - private static final class SnapshotCapture implements BackendBodySnapshotSink { - - private final long expectedBodyId; - @Nullable - private PhysicsBodySnapshot snapshot; - - private SnapshotCapture(long expectedBodyId) { - this.expectedBodyId = expectedBodyId; - } - - @Override - public void accept(long bodyId, - int shapeTypeCode, - int bodyTypeCode, - float positionX, - float positionY, - float positionZ, - float rotationX, - float rotationY, - float rotationZ, - float rotationW, - float linearVelocityX, - float linearVelocityY, - float linearVelocityZ, - float angularVelocityX, - float angularVelocityY, - float angularVelocityZ, - boolean sleeping, - boolean sensor, - float mass, - float friction, - float restitution, - float linearDamping, - float angularDamping, - int collisionGroup, - int collisionMask, - boolean continuousCollisionEnabled, - float centerOfMassOffsetY, - boolean hasBoxHalfExtents, - float halfExtentX, - float halfExtentY, - float halfExtentZ, - float radius, - float halfHeight, - int axisCode) { - if (bodyId != expectedBodyId) { - throw new IllegalStateException("Backend emitted snapshot for unexpected body id " - + bodyId + " while reading " + expectedBodyId); - } - snapshot = fromRuntimeFields(shapeTypeCode, - bodyTypeCode, - positionX, - positionY, - positionZ, - rotationX, - rotationY, - rotationZ, - rotationW, - linearVelocityX, - linearVelocityY, - linearVelocityZ, - angularVelocityX, - angularVelocityY, - angularVelocityZ, - sleeping, - sensor, - mass, - friction, - restitution, - linearDamping, - angularDamping, - collisionGroup, - collisionMask, - continuousCollisionEnabled, - centerOfMassOffsetY, - hasBoxHalfExtents, - halfExtentX, - halfExtentY, - halfExtentZ, - radius, - halfHeight, - axisCode); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodySpatialIndex.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodySpatialIndex.java deleted file mode 100644 index 2ace2343..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodySpatialIndex.java +++ /dev/null @@ -1,280 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.body; - -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.snapshot.PhysicsBodySnapshotEntry; -import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.function.Consumer; -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -/** - * Snapshot-side spatial hash for detached physics bodies. - * - *

Stores the latest published {@link PhysicsBodySnapshot} for each - * {@link RigidBodyKey} and groups those snapshots into fixed-size world cells. - * Cell membership is updated whenever a body publishes a snapshot in a new - * position.

- * - *

Callers use it for area queries that need body identity and pose data, such - * as visual materialization, world-collision streaming hints, diagnostics, and - * other nearby-body discovery. Query freshness follows the snapshot publishing - * policy for each body.

- */ -final class PhysicsBodySpatialIndex { - - private static final float CELL_SIZE = 16.0f; - private static final int AXIS_MASK = 0x1F_FFFF; - - private final Map entries = new Object2ObjectOpenHashMap<>(); - private final Long2ObjectMap> cells = new Long2ObjectOpenHashMap<>(); - private final Int2IntOpenHashMap spaceBodyCounts = new Int2IntOpenHashMap(); - - void update(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - long cellKey = cellKey(snapshot.positionX(), snapshot.positionY(), snapshot.positionZ()); - IndexedBody indexed = entries.get(bodyKey); - if (indexed == null) { - indexed = new IndexedBody(bodyKey, snapshot, spaceId, kind, persistenceMode, cellKey); - entries.put(bodyKey, indexed); - addToCell(indexed, cellKey); - spaceBodyCounts.addTo(spaceId.value(), 1); - return; - } - - if (!indexed.spaceId.equals(spaceId)) { - spaceBodyCounts.addTo(indexed.spaceId.value(), -1); - spaceBodyCounts.addTo(spaceId.value(), 1); - } - if (indexed.cellKey != cellKey) { - removeFromCell(indexed); - addToCell(indexed, cellKey); - } - indexed.snapshot = snapshot; - indexed.spaceId = spaceId; - indexed.kind = kind; - indexed.persistenceMode = persistenceMode; - } - - void remove(@Nonnull RigidBodyKey bodyKey) { - IndexedBody indexed = entries.remove(bodyKey); - if (indexed != null) { - removeFromCell(indexed); - spaceBodyCounts.addTo(indexed.spaceId.value(), -1); - } - } - - void clear() { - entries.clear(); - cells.clear(); - spaceBodyCounts.clear(); - } - - int bodyCount() { - return entries.size(); - } - - int bodyCount(@Nonnull SpaceId spaceId) { - return Math.max(0, spaceBodyCounts.get(spaceId.value())); - } - - int cellCount() { - return cells.size(); - } - - void forEach(@Nonnull SpaceId spaceId, - @Nonnull Consumer consumer) { - for (IndexedBody indexed : entries.values()) { - if (spaceId.equals(indexed.spaceId)) { - consumer.accept(indexed.entry()); - } - } - } - - void forEachIndexed(@Nonnull SpaceId spaceId, - @Nonnull PhysicsBodySnapshotVisitor visitor) { - for (IndexedBody indexed : entries.values()) { - if (spaceId.equals(indexed.spaceId)) { - indexed.visit(visitor); - } - } - } - - int forEachNear(@Nonnull SpaceId spaceId, - @Nonnull Vector3f center, - float radius, - @Nonnull Consumer consumer) { - int minX = cellCoordinate(center.x - radius); - int maxX = cellCoordinate(center.x + radius); - int minY = cellCoordinate(center.y - radius); - int maxY = cellCoordinate(center.y + radius); - int minZ = cellCoordinate(center.z - radius); - int maxZ = cellCoordinate(center.z + radius); - float radiusSquared = radius * radius; - int candidates = 0; - for (int cellX = minX; cellX <= maxX; cellX++) { - for (int cellY = minY; cellY <= maxY; cellY++) { - for (int cellZ = minZ; cellZ <= maxZ; cellZ++) { - List bucket = cells.get(packCell(cellX, cellY, cellZ)); - if (bucket == null) { - continue; - } - for (IndexedBody indexed : bucket) { - if (!spaceId.equals(indexed.spaceId)) { - continue; - } - candidates++; - float dx = indexed.snapshot.positionX() - center.x; - float dy = indexed.snapshot.positionY() - center.y; - float dz = indexed.snapshot.positionZ() - center.z; - if (dx * dx + dy * dy + dz * dz <= radiusSquared) { - consumer.accept(indexed.entry()); - } - } - } - } - } - return candidates; - } - - int forEachIndexedNear(@Nonnull SpaceId spaceId, - @Nonnull Vector3f center, - float radius, - @Nonnull PhysicsBodySnapshotVisitor visitor) { - int minX = cellCoordinate(center.x - radius); - int maxX = cellCoordinate(center.x + radius); - int minY = cellCoordinate(center.y - radius); - int maxY = cellCoordinate(center.y + radius); - int minZ = cellCoordinate(center.z - radius); - int maxZ = cellCoordinate(center.z + radius); - float radiusSquared = radius * radius; - int candidates = 0; - for (int cellX = minX; cellX <= maxX; cellX++) { - for (int cellY = minY; cellY <= maxY; cellY++) { - for (int cellZ = minZ; cellZ <= maxZ; cellZ++) { - List bucket = cells.get(packCell(cellX, cellY, cellZ)); - if (bucket == null) { - continue; - } - for (IndexedBody indexed : bucket) { - if (!spaceId.equals(indexed.spaceId)) { - continue; - } - candidates++; - float dx = indexed.snapshot.positionX() - center.x; - float dy = indexed.snapshot.positionY() - center.y; - float dz = indexed.snapshot.positionZ() - center.z; - if (dx * dx + dy * dy + dz * dz <= radiusSquared) { - indexed.visit(visitor); - } - } - } - } - } - return candidates; - } - - private void removeFromCell(@Nonnull IndexedBody indexed) { - List bucket = cells.get(indexed.cellKey); - if (bucket == null) { - return; - } - int index = indexed.cellIndex; - int lastIndex = bucket.size() - 1; - if (index >= 0 && index <= lastIndex && bucket.get(index) == indexed) { - IndexedBody moved = bucket.get(lastIndex); - bucket.set(index, moved); - moved.cellIndex = index; - bucket.remove(lastIndex); - } else { - bucket.remove(indexed); - } - indexed.cellIndex = -1; - if (bucket.isEmpty()) { - cells.remove(indexed.cellKey); - } - } - - private void addToCell(@Nonnull IndexedBody indexed, long cellKey) { - List bucket = cells.computeIfAbsent(cellKey, ignored -> new ArrayList<>()); - indexed.cellKey = cellKey; - indexed.cellIndex = bucket.size(); - bucket.add(indexed); - } - - private static long cellKey(float positionX, float positionY, float positionZ) { - return packCell(cellCoordinate(positionX), - cellCoordinate(positionY), - cellCoordinate(positionZ)); - } - - private static int cellCoordinate(float value) { - return (int) Math.floor(value / CELL_SIZE); - } - - private static long packCell(int x, int y, int z) { - return ((long) x & AXIS_MASK) << 42 - | ((long) y & AXIS_MASK) << 21 - | ((long) z & AXIS_MASK); - } - - private static final class IndexedBody { - - @Nonnull - private final RigidBodyKey bodyKey; - @Nonnull - private PhysicsBodySnapshot snapshot; - @Nonnull - private SpaceId spaceId; - @Nonnull - private PhysicsBodyKind kind; - @Nonnull - private PhysicsBodyPersistenceMode persistenceMode; - private long cellKey; - private int cellIndex = -1; - - private IndexedBody(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode, - long cellKey) { - this.bodyKey = bodyKey; - this.snapshot = snapshot; - this.spaceId = spaceId; - this.kind = kind; - this.persistenceMode = persistenceMode; - this.cellKey = cellKey; - } - - @Nonnull - private PhysicsBodySnapshotEntry entry() { - return new PhysicsBodySnapshotEntry(bodyKey, - snapshot, - spaceId, - kind, - persistenceMode); - } - - private void visit(@Nonnull PhysicsBodySnapshotVisitor visitor) { - visitor.accept(bodyKey, - snapshot, - spaceId, - kind, - persistenceMode); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/joint/PhysicsJointRegistration.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/joint/PhysicsJointRegistration.java deleted file mode 100644 index e0606c26..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/joint/PhysicsJointRegistration.java +++ /dev/null @@ -1,36 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.joint; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.resources.BackendJointHandle; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.JointType; -import javax.annotation.Nonnull; - -/** - * Owner-lane registration for a stable joint key and backend-local joint handle. - */ -public record PhysicsJointRegistration(@Nonnull JointKey jointKey, - @Nonnull BackendJointHandle backendJointHandle, - @Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB, - @Nonnull JointType type, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ, - float restLength, - float stiffness, - float damping, - float lowerLimit, - float upperLimit, - boolean motorEnabled, - float motorTargetVelocity, - float motorMaxForce) { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/joint/PhysicsJointRegistry.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/joint/PhysicsJointRegistry.java deleted file mode 100644 index dbe2d75d..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/joint/PhysicsJointRegistry.java +++ /dev/null @@ -1,201 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.joint; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.resources.BackendJointHandle; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.simulation.JointType; -import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Map; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Runtime identity index for backend physics joints. - */ -public final class PhysicsJointRegistry { - - private final Map registrationsByKey = - new Object2ObjectLinkedOpenHashMap<>(); - private final Int2ObjectOpenHashMap> jointKeysByRawBackendId = - new Int2ObjectOpenHashMap<>(); - - @Nonnull - public PhysicsJointRegistration registerJoint(@Nonnull JointKey jointKey, - @Nonnull SpaceId spaceId, - @Nonnull BackendJointHandle backendJointHandle, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB, - @Nonnull JointType type, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ, - float restLength, - float stiffness, - float damping, - float lowerLimit, - float upperLimit, - boolean motorEnabled, - float motorTargetVelocity, - float motorMaxForce) { - JointKey existingKey = getJointKey(spaceId, backendJointHandle); - if (existingKey != null && !existingKey.equals(jointKey)) { - throw new IllegalArgumentException("Physics joint is already registered as " + existingKey); - } - PhysicsJointRegistration existingRegistration = registrationsByKey.get(jointKey); - if (existingRegistration != null - && (!existingRegistration.backendJointHandle().equals(backendJointHandle) - || !existingRegistration.spaceId().equals(spaceId))) { - throw new IllegalArgumentException("Physics joint key=" + jointKey - + " is already registered to another backend joint"); - } - PhysicsJointRegistration registration = new PhysicsJointRegistration(jointKey, - backendJointHandle, - spaceId, - bodyA, - bodyB, - type, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - axisX, - axisY, - axisZ, - restLength, - stiffness, - damping, - lowerLimit, - upperLimit, - motorEnabled, - motorTargetVelocity, - motorMaxForce); - registrationsByKey.put(jointKey, registration); - jointKeysByRawBackendId - .computeIfAbsent(spaceId.value(), ignored -> new Long2ObjectOpenHashMap<>()) - .put(backendJointHandle.value(), jointKey); - return registration; - } - - @Nullable - public PhysicsJointRegistration unregisterJoint(@Nonnull JointKey jointKey) { - PhysicsJointRegistration registration = registrationsByKey.remove(jointKey); - if (registration == null) { - return null; - } - - removeBackendIndex(registration); - return registration; - } - - @Nullable - public PhysicsJointRegistration unregisterJoint(@Nonnull SpaceId spaceId, long backendJointId) { - JointKey jointKey = getJointKey(spaceId, backendJointId); - return jointKey != null ? unregisterJoint(jointKey) : null; - } - - @Nonnull - public Collection unregisterJointsForBody(@Nonnull RigidBodyKey bodyKey) { - ArrayList removed = new ArrayList<>(); - for (PhysicsJointRegistration registration : registrationsByKey.values()) { - if (registration.bodyA().equals(bodyKey) || registration.bodyB().equals(bodyKey)) { - removed.add(registration.jointKey()); - } - } - ArrayList registrations = new ArrayList<>(removed.size()); - for (JointKey jointKey : removed) { - PhysicsJointRegistration registration = unregisterJoint(jointKey); - if (registration != null) { - registrations.add(registration); - } - } - return registrations; - } - - public void unregisterSpace(@Nonnull SpaceId spaceId) { - ArrayList removed = new ArrayList<>(); - for (PhysicsJointRegistration registration : registrationsByKey.values()) { - if (registration.spaceId().equals(spaceId)) { - removed.add(registration.jointKey()); - } - } - for (JointKey jointKey : removed) { - unregisterJoint(jointKey); - } - } - - @Nullable - public PhysicsJointRegistration getRegistration(@Nonnull JointKey jointKey) { - return registrationsByKey.get(jointKey); - } - - @Nullable - public JointKey getJointKey(@Nonnull SpaceId spaceId, long backendJointId) { - Long2ObjectOpenHashMap jointKeys = - jointKeysByRawBackendId.get(spaceId.value()); - return jointKeys != null ? jointKeys.get(backendJointId) : null; - } - - @Nullable - public JointKey getJointKey(@Nonnull SpaceId spaceId, - @Nonnull BackendJointHandle backendJointHandle) { - return getJointKey(spaceId, backendJointHandle.value()); - } - - @Nullable - public PhysicsJointRegistration findJointBetween(@Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB) { - for (PhysicsJointRegistration registration : registrationsByKey.values()) { - if (!registration.spaceId().equals(spaceId)) { - continue; - } - if (connects(bodyA, bodyB, registration.bodyA(), registration.bodyB())) { - return registration; - } - } - return null; - } - - @Nonnull - public Collection getRegistrations() { - return new ArrayList<>(registrationsByKey.values()); - } - - public void clear() { - registrationsByKey.clear(); - jointKeysByRawBackendId.clear(); - } - - private void removeBackendIndex(@Nonnull PhysicsJointRegistration registration) { - Long2ObjectOpenHashMap jointKeys = - jointKeysByRawBackendId.get(registration.spaceId().value()); - if (jointKeys == null) { - return; - } - jointKeys.remove(registration.backendJointHandle().value()); - if (jointKeys.isEmpty()) { - jointKeysByRawBackendId.remove(registration.spaceId().value()); - } - } - - private static boolean connects(@Nonnull RigidBodyKey expectedA, - @Nonnull RigidBodyKey expectedB, - @Nonnull RigidBodyKey actualA, - @Nonnull RigidBodyKey actualB) { - return (expectedA.equals(actualA) && expectedB.equals(actualB)) - || (expectedA.equals(actualB) && expectedB.equals(actualA)); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsCommandVisibilityState.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsCommandVisibilityState.java deleted file mode 100644 index 9ee5ff87..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsCommandVisibilityState.java +++ /dev/null @@ -1,139 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import dev.hytalemodding.impulse.core.internal.simulation.batch.RecordedPhysicsCommandBatch; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; -import java.util.concurrent.atomic.AtomicLong; -import javax.annotation.Nonnull; - -/** - * Tracks command execution state that controls when command-created bodies are visible to readers. - */ -public final class PhysicsCommandVisibilityState { - - /* - * Monotonic owner-command sequence assigned at submission. It is separate from owner step - * sequence and Hytale world ticks because commands can complete between published snapshots. - */ - private final AtomicLong commandBatchSequence = new AtomicLong(); - - /* - * Highest command batch that has finished owner-lane execution. Snapshot capture copies this - * value as the frame's last-included command-batch sequence. - */ - private final AtomicLong completedCommandBatchSequence = new AtomicLong(); - - /* - * Reader-side body materialization can observe command completion before the next published - * registration frame has applied. Track exact command-created body keys so missing unrelated - * registrations can still be cleaned up during that publication gap. - */ - private final Object2LongOpenHashMap pendingCommandBodyCreationSequences = - new Object2LongOpenHashMap<>(); - - /* - * Command contexts capture this epoch. Runtime resets and topology replacement increment it so - * stale pre-reset command batches reject before dispatching against the new world state. - */ - private final AtomicLong commandWorldEpoch = new AtomicLong(); - - /* - * Topology changes inside a command batch should update snapshot world epoch, but should not - * make the next same-epoch FIFO command batch stale. This depth suppresses command epoch bumps - * during command execution. - */ - private final ThreadLocal commandBatchExecutionDepth = ThreadLocal.withInitial(() -> 0); - - public long nextCommandBatchSequence() { - return commandBatchSequence.incrementAndGet(); - } - - public long commandWorldEpoch() { - return commandWorldEpoch.get(); - } - - public boolean markWorldChanged() { - if (commandBatchExecutionDepth.get() != 0) { - return false; - } - commandWorldEpoch.incrementAndGet(); - return true; - } - - public int enterCommandBatchExecution() { - int depth = commandBatchExecutionDepth.get(); - commandBatchExecutionDepth.set(depth + 1); - return depth; - } - - public void exitCommandBatchExecution(int previousDepth) { - if (previousDepth == 0) { - commandBatchExecutionDepth.remove(); - } else { - commandBatchExecutionDepth.set(previousDepth); - } - } - - public void markCommandBatchCompleted(long commandBatchSequence) { - completedCommandBatchSequence.accumulateAndGet(commandBatchSequence, Math::max); - } - - public long completedCommandBatchSequence() { - return completedCommandBatchSequence.get(); - } - - public boolean trackBodyCreationPublication(@Nonnull RecordedPhysicsCommandBatch batch, - boolean ownerExecutorAttached) { - if (!batch.hasBodyCreationCommands() || !ownerExecutorAttached) { - return false; - } - long commandBatchSequence = batch.metadata().commandBatchSequence(); - synchronized (pendingCommandBodyCreationSequences) { - for (int index = 0; index < batch.bodyCreationKeyCount(); index++) { - RigidBodyKey bodyKey = batch.bodyCreationKey(index); - pendingCommandBodyCreationSequences.put(bodyKey, - Math.max(pendingCommandBodyCreationSequences.getLong(bodyKey), - commandBatchSequence)); - } - } - return true; - } - - public void clearBodyCreationPublication(@Nonnull RecordedPhysicsCommandBatch batch) { - long commandBatchSequence = batch.metadata().commandBatchSequence(); - synchronized (pendingCommandBodyCreationSequences) { - for (int index = 0; index < batch.bodyCreationKeyCount(); index++) { - RigidBodyKey bodyKey = batch.bodyCreationKey(index); - long current = pendingCommandBodyCreationSequences.getLong(bodyKey); - if (current <= commandBatchSequence) { - pendingCommandBodyCreationSequences.removeLong(bodyKey); - } - } - } - } - - public void clearBodyCreationPending(@Nonnull RigidBodyKey bodyKey) { - synchronized (pendingCommandBodyCreationSequences) { - pendingCommandBodyCreationSequences.removeLong(bodyKey); - } - } - - public void applyLastIncludedCommandBatchSequence(long lastIncludedCommandBatchSequence) { - synchronized (pendingCommandBodyCreationSequences) { - pendingCommandBodyCreationSequences.object2LongEntrySet() - .removeIf(entry -> entry.getLongValue() <= lastIncludedCommandBatchSequence); - } - } - - public boolean isBodyCreationPending(@Nonnull RigidBodyKey bodyKey, - boolean directBodyCreationPending) { - return directBodyCreationPending - || isCommandBodyCreationPending(bodyKey); - } - - private boolean isCommandBodyCreationPending(@Nonnull RigidBodyKey bodyKey) { - synchronized (pendingCommandBodyCreationSequences) { - return pendingCommandBodyCreationSequences.containsKey(bodyKey); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerBridge.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerBridge.java deleted file mode 100644 index b0cea2f3..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerBridge.java +++ /dev/null @@ -1,226 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.atomic.AtomicReference; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Synchronous bridge for code paths that must mutate live physics state in the - * world physics owner context. - */ -public final class PhysicsOwnerBridge { - - private PhysicsOwnerBridge() { - } - - public static void run(@Nonnull Store store, - @Nonnull String operation, - @Nonnull OwnerMutation mutation) { - run(owner(store), operation, mutation); - } - - public static void run(@Nonnull PhysicsOwnerResource owner, - @Nonnull String operation, - @Nonnull OwnerMutation mutation) { - call(owner, operation, () -> { - mutation.run(); - return null; - }); - } - - @Nonnull - public static PhysicsMutationHandle runAsync(@Nonnull Store store, - @Nonnull String operation, - @Nonnull OwnerMutation mutation) { - return runAsync(owner(store), operation, mutation); - } - - @Nonnull - public static PhysicsMutationHandle runAsync(@Nonnull PhysicsOwnerResource owner, - @Nonnull String operation, - @Nonnull OwnerMutation mutation) { - return runAsync(owner, operation, null, mutation); - } - - @Nonnull - public static PhysicsMutationHandle runAsync(@Nonnull Store store, - @Nonnull String operation, - @Nullable T value, - @Nonnull OwnerMutation mutation) { - return runAsync(owner(store), operation, value, mutation); - } - - @Nonnull - public static PhysicsMutationHandle runAsync(@Nonnull PhysicsOwnerResource owner, - @Nonnull String operation, - @Nullable T value, - @Nonnull OwnerMutation mutation) { - Objects.requireNonNull(owner, "owner"); - Objects.requireNonNull(operation, "operation"); - Objects.requireNonNull(mutation, "mutation"); - if (owner.isOwnerContext()) { - return runInlineAsync(operation, value, mutation); - } - - PhysicsOwnerCommand command = () -> { - mutation.run(); - return PhysicsOwnerSnapshot.empty(); - }; - try { - return owner.submitMutation(operation, value, command); - } catch (RejectedExecutionException exception) { - return PhysicsMutationHandle.failed(operation, value, exception); - } - } - - @Nonnull - public static T call(@Nonnull Store store, - @Nonnull String operation, - @Nonnull OwnerCallable callable) { - return call(owner(store), operation, callable); - } - - @Nonnull - public static T call(@Nonnull PhysicsOwnerResource owner, - @Nonnull String operation, - @Nonnull OwnerCallable callable) { - Objects.requireNonNull(owner, "owner"); - Objects.requireNonNull(operation, "operation"); - Objects.requireNonNull(callable, "callable"); - if (owner.isOwnerContext()) { - return callInline(operation, callable); - } - - AtomicReference value = new AtomicReference<>(); - PhysicsOwnerCommand command = () -> { - value.set(callable.call()); - return PhysicsOwnerSnapshot.empty(); - }; - try { - owner.submitAndDrain(command); - return value.get(); - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - throw new IllegalStateException("Interrupted while running physics owner lane operation " - + operation, exception); - } catch (ExecutionException exception) { - throw ownerFailure(operation, exception.getCause()); - } catch (RejectedExecutionException exception) { - throw ownerFailure(operation, exception); - } - } - - @Nonnull - public static CompletableFuture callAsync(@Nonnull PhysicsOwnerResource owner, - @Nonnull String operation, - @Nonnull OwnerCallable callable) { - Objects.requireNonNull(owner, "owner"); - Objects.requireNonNull(operation, "operation"); - Objects.requireNonNull(callable, "callable"); - if (owner.isOwnerContext()) { - return callInlineAsync(callable); - } - - CompletableFuture completion = new CompletableFuture<>(); - AtomicReference value = new AtomicReference<>(); - PhysicsOwnerCommand command = () -> { - value.set(callable.call()); - return PhysicsOwnerSnapshot.empty(); - }; - try { - owner.submitMutationFuture(operation, command) - .whenComplete((ignored, failure) -> { - if (failure != null) { - completion.completeExceptionally(failure); - } else { - completion.complete(value.get()); - } - }); - } catch (RejectedExecutionException exception) { - completion.completeExceptionally(exception); - } - return completion; - } - - @Nonnull - private static PhysicsOwnerResource owner(@Nonnull Store store) { - return store.getResource(PhysicsOwnerResource.getResourceType()); - } - - @Nonnull - private static T callInline(@Nonnull String operation, - @Nonnull OwnerCallable callable) { - try { - return callable.call(); - } catch (RuntimeException exception) { - throw exception; - } catch (Exception exception) { - throw new IllegalStateException(ownerFailureMessage(operation, exception), exception); - } - } - - @Nonnull - private static PhysicsMutationHandle runInlineAsync(@Nonnull String operation, - @Nullable T value, - @Nonnull OwnerMutation mutation) { - try { - mutation.run(); - return PhysicsMutationHandle.completed(operation, value); - } catch (Throwable throwable) { - return PhysicsMutationHandle.failed(operation, value, throwable); - } - } - - @Nonnull - private static CompletableFuture callInlineAsync(@Nonnull OwnerCallable callable) { - try { - return CompletableFuture.completedFuture(callable.call()); - } catch (Throwable throwable) { - CompletableFuture completion = new CompletableFuture<>(); - completion.completeExceptionally(throwable); - return completion; - } - } - - @Nonnull - private static RuntimeException ownerFailure(@Nonnull String operation, - @Nonnull Throwable cause) { - if (cause instanceof RuntimeException runtimeException) { - return runtimeException; - } - return new IllegalStateException(ownerFailureMessage(operation, cause), cause); - } - - @Nonnull - private static String ownerFailureMessage(@Nonnull String operation, - @Nonnull Throwable cause) { - StringBuilder message = new StringBuilder("Physics owner lane operation ") - .append(operation) - .append(" failed: ") - .append(cause.getClass().getSimpleName()); - String causeMessage = cause.getMessage(); - if (causeMessage != null && !causeMessage.isBlank()) { - message.append(": ").append(causeMessage); - } - return message.toString(); - } - - @FunctionalInterface - public interface OwnerCallable { - - T call() throws Exception; - } - - @FunctionalInterface - public interface OwnerMutation { - - void run() throws Exception; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerCallable.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerCallable.java deleted file mode 100644 index 8ae5129c..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerCallable.java +++ /dev/null @@ -1,12 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -/** - * Internal operation that runs in the current live-backend owner context and returns a value. - * - * @param value returned to the caller - */ -@FunctionalInterface -public interface PhysicsOwnerCallable { - - T call() throws Exception; -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerCommand.java deleted file mode 100644 index aa8b322d..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerCommand.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import javax.annotation.Nonnull; - -/** - * Unit of work executed within a serialized physics owner context. - */ -@FunctionalInterface -public interface PhysicsOwnerCommand { - - @Nonnull - PhysicsOwnerSnapshot run() throws Exception; -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerExecutor.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerExecutor.java deleted file mode 100644 index 6fc4b29e..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerExecutor.java +++ /dev/null @@ -1,44 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import java.util.concurrent.CompletableFuture; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Internal serialized owner lane for one world physics resource. - * - *

Callbacks routed through this executor may touch backend runtime state. Implementations define - * owner-context membership with their own lane token; callers must not assume a stable Java thread - * owns a world.

- */ -public interface PhysicsOwnerExecutor { - - /** - * Returns whether the current thread is already executing this exact owner lane. - */ - boolean isOwnerContext(); - - /** - * Returns whether the current thread is running a user-visible owner completion callback. - */ - default boolean isCompletionCallbackContext() { - return false; - } - - void run(@Nonnull String operation, - @Nonnull PhysicsOwnerMutation mutation); - - @Nonnull - PhysicsMutationHandle enqueue(@Nonnull String operation, - @Nullable T value, - @Nonnull PhysicsOwnerMutation mutation); - - @Nonnull - CompletableFuture enqueueCall(@Nonnull String operation, - @Nonnull PhysicsOwnerCallable callable); - - @Nonnull - T call(@Nonnull String operation, - @Nonnull PhysicsOwnerCallable callable); -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerGateway.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerGateway.java deleted file mode 100644 index b4b8a81b..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerGateway.java +++ /dev/null @@ -1,167 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.atomic.AtomicReference; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Internal owner-lane gateway for one world physics resource. - * - *

This class centralizes owner-context routing while {@code PhysicsWorldResource} remains the - * plugin-facing facade. Callbacks routed through this gateway may touch backend runtime state; - * ordinary world-thread reads should use published snapshots instead.

- */ -public final class PhysicsOwnerGateway { - - private final AtomicReference ownerExecutor = new AtomicReference<>(); - - public void attachOwnerExecutor(@Nonnull PhysicsOwnerExecutor ownerExecutor) { - this.ownerExecutor.set(Objects.requireNonNull(ownerExecutor, "ownerExecutor")); - } - - public void detachOwnerExecutor(@Nonnull PhysicsOwnerExecutor ownerExecutor) { - this.ownerExecutor.compareAndSet(Objects.requireNonNull(ownerExecutor, "ownerExecutor"), null); - } - - /** - * Returns whether the current thread may touch live backend objects without routing. - */ - public boolean canAccessLiveBackendDirectly() { - PhysicsOwnerExecutor executor = ownerExecutor.get(); - return executor == null || executor.isOwnerContext(); - } - - public boolean hasOwnerExecutor() { - return ownerExecutor.get() != null; - } - - public void assertCanAccessLiveBackendDirectly(@Nonnull String operation) { - Objects.requireNonNull(operation, "operation"); - if (!canAccessLiveBackendDirectly()) { - throw new IllegalStateException("Impulse live backend operation " + operation - + " must run in the physics owner lane. Use copied simulation commands, " - + "copied queries, or an internal owner-routed resource method."); - } - } - - public void rejectSynchronousCompletionCallbackWait(@Nonnull String operation) { - Objects.requireNonNull(operation, "operation"); - PhysicsOwnerExecutor executor = ownerExecutor.get(); - if (executor != null && executor.isCompletionCallbackContext()) { - throw new RejectedExecutionException( - "cannot synchronously wait for physics owner operation " + operation - + " from a completion callback"); - } - } - - public void run(@Nonnull String operation, - @Nonnull PhysicsOwnerMutation mutation) { - Objects.requireNonNull(operation, "operation"); - Objects.requireNonNull(mutation, "mutation"); - PhysicsOwnerExecutor executor = ownerExecutor.get(); - if (executor == null || executor.isOwnerContext()) { - runDirect(operation, mutation); - return; - } - executor.run(operation, mutation); - } - - @Nonnull - public PhysicsMutationHandle enqueue(@Nonnull String operation, - @Nonnull PhysicsOwnerMutation mutation) { - return enqueue(operation, null, mutation); - } - - @Nonnull - public PhysicsMutationHandle enqueue(@Nonnull String operation, - @Nullable T value, - @Nonnull PhysicsOwnerMutation mutation) { - Objects.requireNonNull(operation, "operation"); - Objects.requireNonNull(mutation, "mutation"); - PhysicsOwnerExecutor executor = ownerExecutor.get(); - if (executor == null || executor.isOwnerContext()) { - return runDirectAsync(operation, value, mutation); - } - try { - return executor.enqueue(operation, value, mutation); - } catch (RejectedExecutionException exception) { - return PhysicsMutationHandle.failed(operation, value, exception); - } - } - - @Nonnull - public CompletableFuture enqueueCall(@Nonnull String operation, - @Nonnull PhysicsOwnerCallable callable) { - Objects.requireNonNull(operation, "operation"); - Objects.requireNonNull(callable, "callable"); - PhysicsOwnerExecutor executor = ownerExecutor.get(); - if (executor == null || executor.isOwnerContext()) { - return callDirectAsync(callable); - } - try { - return executor.enqueueCall(operation, callable); - } catch (RejectedExecutionException exception) { - CompletableFuture completion = new CompletableFuture<>(); - completion.completeExceptionally(exception); - return completion; - } - } - - @Nonnull - public T call(@Nonnull String operation, - @Nonnull PhysicsOwnerCallable callable) { - Objects.requireNonNull(operation, "operation"); - Objects.requireNonNull(callable, "callable"); - PhysicsOwnerExecutor executor = ownerExecutor.get(); - if (executor == null || executor.isOwnerContext()) { - try { - return callable.call(); - } catch (RuntimeException exception) { - throw exception; - } catch (Exception exception) { - throw new IllegalStateException("Physics operation " + operation + " failed", - exception); - } - } - return executor.call(operation, callable); - } - - private static void runDirect(@Nonnull String operation, - @Nonnull PhysicsOwnerMutation mutation) { - try { - mutation.run(); - } catch (RuntimeException exception) { - throw exception; - } catch (Exception exception) { - throw new IllegalStateException("Physics operation " + operation + " failed", - exception); - } - } - - @Nonnull - private static PhysicsMutationHandle runDirectAsync(@Nonnull String operation, - @Nullable T value, - @Nonnull PhysicsOwnerMutation mutation) { - try { - mutation.run(); - return PhysicsMutationHandle.completed(operation, value); - } catch (Throwable throwable) { - return PhysicsMutationHandle.failed(operation, value, throwable); - } - } - - @Nonnull - private static CompletableFuture callDirectAsync(@Nonnull PhysicsOwnerCallable callable) { - try { - return CompletableFuture.completedFuture(callable.call()); - } catch (Throwable throwable) { - CompletableFuture completion = new CompletableFuture<>(); - completion.completeExceptionally(throwable); - return completion; - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerHandle.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerHandle.java deleted file mode 100644 index 5c2a0a09..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerHandle.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -/** - * Internal opaque owner lane for live physics backend state. - */ -public interface PhysicsOwnerHandle extends PhysicsOwnerExecutor { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerLaneResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerLaneResource.java deleted file mode 100644 index 2444835b..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerLaneResource.java +++ /dev/null @@ -1,803 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerCommand; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerMutationCompletion; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerResult; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerSnapshot; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerStepCommand; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerStepCompletion; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import java.time.Duration; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Per-world owner lane backed by a shared {@link PhysicsOwnerLaneScheduler}. - */ -public final class PhysicsOwnerLaneResource implements PhysicsOwnerResource { - - private final Object lock = new Object(); - private final PhysicsOwnerLaneScheduler scheduler; - private final int queueCapacity; - @Nonnull - private final Duration closeTimeout; - private final ArrayDeque mutationQueue = new ArrayDeque<>(); - private final ArrayDeque stepQueue = new ArrayDeque<>(); - private final ArrayDeque pendingMutations = new ArrayDeque<>(); - private long nextSequence = 1L; - private boolean started; - private boolean accepting; - private boolean closing; - private boolean closed; - private boolean active; - private boolean activePreStepDrain; - private int activeCompletions; - @Nullable - private QueuedCommand activeCommand; - @Nullable - private PendingStep pendingStep; - - PhysicsOwnerLaneResource(@Nonnull PhysicsOwnerLaneScheduler scheduler, - int queueCapacity, - @Nonnull Duration closeTimeout) { - if (queueCapacity < 1) { - throw new IllegalArgumentException("queueCapacity must be positive"); - } - this.scheduler = Objects.requireNonNull(scheduler, "scheduler"); - this.queueCapacity = queueCapacity; - this.closeTimeout = Objects.requireNonNull(closeTimeout, "closeTimeout"); - } - - @Override - public void start(@Nonnull String worldName) { - Objects.requireNonNull(worldName, "worldName"); - synchronized (lock) { - if (closed || started) { - return; - } - started = true; - accepting = true; - } - } - - @Override - public boolean isStarted() { - synchronized (lock) { - return started && accepting && !closed; - } - } - - @Override - public boolean isClosed() { - synchronized (lock) { - return closed; - } - } - - @Nonnull - @Override - public PhysicsOwnerResult submitAndDrain(@Nonnull PhysicsOwnerCommand command) - throws InterruptedException, ExecutionException { - Objects.requireNonNull(command, "command"); - if (isOwnerContext()) { - return runInline(command); - } - rejectSynchronousCompletionCallbackWait(); - rejectSynchronousCrossLaneWait(); - - CompletableFuture future; - synchronized (lock) { - future = enqueueLocked(CommandKind.MUTATION, command).future(); - dispatchIfIdleLocked(); - } - return future.get(); - } - - @Override - public boolean submitStepIfIdle(@Nonnull PhysicsOwnerCommand command) { - Objects.requireNonNull(command, "command"); - if (!(command instanceof PhysicsOwnerStepCommand stepCommand)) { - throw new IllegalArgumentException("Physics owner step command must capture step metadata"); - } - synchronized (lock) { - requireAcceptingLocked(); - if (pendingStep != null) { - return false; - } - QueuedCommand queuedCommand = enqueueLocked(CommandKind.STEP, stepCommand); - pendingStep = new PendingStep(stepCommand, - queuedCommand.future(), - queuedCommand.submittedNanos()); - dispatchIfIdleLocked(); - return true; - } - } - - @Nonnull - @Override - public PhysicsMutationHandle submitMutation(@Nonnull String operation, - @Nonnull PhysicsOwnerCommand command) { - return submitMutation(operation, null, command); - } - - @Nonnull - @Override - public PhysicsMutationHandle submitMutation(@Nonnull String operation, - @Nullable T value, - @Nonnull PhysicsOwnerCommand command) { - CompletableFuture future = submitMutationFuture(operation, command); - return PhysicsMutationHandle.fromCompletion(operation, value, future); - } - - @Nonnull - @Override - public CompletableFuture submitMutationFuture(@Nonnull String operation, - @Nonnull PhysicsOwnerCommand command) { - Objects.requireNonNull(operation, "operation"); - Objects.requireNonNull(command, "command"); - synchronized (lock) { - if (pendingMutations.size() >= queueCapacity) { - throw new RejectedExecutionException("physics owner lane mutation completion backlog is full"); - } - CompletableFuture future = - enqueueLocked(CommandKind.MUTATION, command).future(); - pendingMutations.add(new PendingMutation(operation, future)); - dispatchIfIdleLocked(); - return future; - } - } - - @Nonnull - @Override - public List pollCompletedMutations(int maxCompletions) { - int limit = Math.max(0, maxCompletions); - if (limit == 0) { - return Collections.emptyList(); - } - synchronized (lock) { - PendingMutation first = pendingMutations.peek(); - if (first == null || !first.future().isDone()) { - return Collections.emptyList(); - } - List completions = new ArrayList<>(limit); - while (completions.size() < limit) { - PendingMutation current = pendingMutations.peek(); - if (current == null || !current.future().isDone()) { - break; - } - pendingMutations.poll(); - completions.add(toMutationCompletion(current)); - } - return completions; - } - } - - @Nullable - @Override - public PhysicsOwnerStepCompletion pollCompletedStep() { - PendingStep completed; - synchronized (lock) { - PendingStep current = pendingStep; - if (current == null || !current.future().isDone()) { - return null; - } - pendingStep = null; - completed = current; - } - - try { - PhysicsOwnerResult result = completed.future().get(); - return new PhysicsOwnerStepCompletion(result, - completed.command().publishedFrame(), - completed.command().eventFrame(), - completed.command().failure(), - completed.preStepDrainedMutations(), - completed.preStepDrainRunNanos(), - completed.lateMutationBacklogAtStep(), - null); - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - return new PhysicsOwnerStepCompletion(null, - null, - null, - null, - completed.preStepDrainedMutations(), - completed.preStepDrainRunNanos(), - completed.lateMutationBacklogAtStep(), - exception); - } catch (ExecutionException exception) { - return new PhysicsOwnerStepCompletion(null, - null, - null, - null, - completed.preStepDrainedMutations(), - completed.preStepDrainRunNanos(), - completed.lateMutationBacklogAtStep(), - exception.getCause()); - } - } - - @Override - public boolean hasPendingStep() { - synchronized (lock) { - return pendingStep != null; - } - } - - @Override - public long pendingStepAgeNanos() { - synchronized (lock) { - PendingStep current = pendingStep; - return current == null ? 0L : Math.max(0L, System.nanoTime() - current.submittedNanos()); - } - } - - @Override - public int pendingMutations() { - synchronized (lock) { - return pendingMutations.size(); - } - } - - @Override - public int pendingCommands() { - synchronized (lock) { - return queuedCommandCountLocked() + (activeCommandPendingLocked() ? 1 : 0); - } - } - - @Override - public boolean isOwnerContext() { - return scheduler.isCurrentLane(this); - } - - @Override - public boolean isCompletionCallbackContext() { - return scheduler.isCompletingAnyLane(); - } - - @Override - public void run(@Nonnull String operation, - @Nonnull PhysicsOwnerMutation mutation) { - Objects.requireNonNull(operation, "operation"); - Objects.requireNonNull(mutation, "mutation"); - if (isOwnerContext()) { - runDirect(operation, mutation); - return; - } - rejectSynchronousCompletionCallbackWait(); - rejectSynchronousCrossLaneWait(); - try { - submitAndDrain(() -> { - mutation.run(); - return PhysicsOwnerSnapshot.empty(); - }); - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - throw new IllegalStateException("Interrupted while running physics owner lane operation " - + operation, exception); - } catch (ExecutionException exception) { - throw ownerFailure(operation, exception.getCause()); - } - } - - @Nonnull - @Override - public PhysicsMutationHandle enqueue(@Nonnull String operation, - @Nullable T value, - @Nonnull PhysicsOwnerMutation mutation) { - Objects.requireNonNull(operation, "operation"); - Objects.requireNonNull(mutation, "mutation"); - if (isOwnerContext()) { - return runDirectAsync(operation, value, mutation); - } - return submitMutation(operation, value, () -> { - mutation.run(); - return PhysicsOwnerSnapshot.empty(); - }); - } - - @Nonnull - @Override - public CompletableFuture enqueueCall(@Nonnull String operation, - @Nonnull PhysicsOwnerCallable callable) { - Objects.requireNonNull(operation, "operation"); - Objects.requireNonNull(callable, "callable"); - if (isOwnerContext()) { - return callDirectAsync(callable); - } - - CompletableFuture completion = new CompletableFuture<>(); - AtomicReference value = new AtomicReference<>(); - try { - submitMutationFuture(operation, () -> { - value.set(callable.call()); - return PhysicsOwnerSnapshot.empty(); - }).whenComplete((ignored, failure) -> { - if (failure != null) { - completion.completeExceptionally(failure); - } else { - completion.complete(value.get()); - } - }); - } catch (RejectedExecutionException exception) { - completion.completeExceptionally(exception); - } - return completion; - } - - @Nonnull - @Override - public T call(@Nonnull String operation, - @Nonnull PhysicsOwnerCallable callable) { - Objects.requireNonNull(operation, "operation"); - Objects.requireNonNull(callable, "callable"); - if (isOwnerContext()) { - return callDirect(operation, callable); - } - rejectSynchronousCompletionCallbackWait(); - rejectSynchronousCrossLaneWait(); - - AtomicReference value = new AtomicReference<>(); - try { - submitAndDrain(() -> { - value.set(callable.call()); - return PhysicsOwnerSnapshot.empty(); - }); - return value.get(); - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - throw new IllegalStateException("Interrupted while running physics owner lane operation " - + operation, exception); - } catch (ExecutionException exception) { - throw ownerFailure(operation, exception.getCause()); - } - } - - @Override - public void close() { - if (!isClosed()) { - rejectSynchronousCompletionCallbackWait(); - rejectSynchronousCrossLaneWait(); - rejectSynchronousOwnerContextWait(); - } - - synchronized (lock) { - if (closed) { - return; - } - closing = true; - accepting = false; - dispatchIfIdleLocked(); - } - - RuntimeException timeout = null; - long timeoutNanos = Math.max(0L, closeTimeout.toNanos()); - long deadline = timeoutNanos == 0L - ? Long.MAX_VALUE - : System.nanoTime() + timeoutNanos; - boolean interrupted = false; - synchronized (lock) { - while (!closed && (active - || activeCompletions > 0 - || !mutationQueue.isEmpty() - || !stepQueue.isEmpty())) { - long remainingNanos = deadline - System.nanoTime(); - if (remainingNanos <= 0L) { - timeout = new IllegalStateException( - "Physics owner lane did not stop within " + closeTimeout); - break; - } - try { - long millis = TimeUnit.NANOSECONDS.toMillis(remainingNanos); - int nanos = (int) (remainingNanos - TimeUnit.MILLISECONDS.toNanos(millis)); - lock.wait(millis, nanos); - } catch (InterruptedException exception) { - interrupted = true; - } - } - if (timeout == null && !closed) { - closeIfReadyLocked(); - if (!closed) { - closed = true; - scheduler.unregister(this); - lock.notifyAll(); - } - } - } - if (interrupted) { - Thread.currentThread().interrupt(); - } - if (timeout != null) { - throw timeout; - } - } - - @Nonnull - @Override - public PhysicsOwnerLaneResource clone() { - return scheduler.createLane(); - } - - private void runQueuedCommand(@Nonnull QueuedCommand command) { - long startNanos = System.nanoTime(); - PhysicsOwnerResult result = null; - Throwable failure = null; - long commandRunNanos; - try { - PhysicsOwnerSnapshot snapshot = command.command().run(); - long completedNanos = System.nanoTime(); - result = new PhysicsOwnerResult(command.sequence(), - snapshot, - startNanos - command.submittedNanos(), - completedNanos - startNanos, - completedNanos); - commandRunNanos = result.runNanos(); - } catch (Throwable throwable) { - failure = throwable; - commandRunNanos = Math.max(0L, System.nanoTime() - startNanos); - } - - completeCommandFuture(command, result, failure); - - synchronized (lock) { - if (activePreStepDrain && pendingStep != null) { - pendingStep.recordPreStepDrain(commandRunNanos); - } - active = false; - activePreStepDrain = false; - activeCommand = null; - closeIfReadyLocked(); - dispatchIfIdleLocked(); - lock.notifyAll(); - } - } - - private void completeCommandFuture(@Nonnull QueuedCommand command, - @Nullable PhysicsOwnerResult result, - @Nullable Throwable failure) { - recordCompletionScheduled(); - scheduler.completeOutsideOwnerContext(this, () -> { - try { - if (failure == null) { - command.future().complete(result); - } else { - command.future().completeExceptionally(failure); - } - } finally { - recordCompletionFinished(); - } - }); - } - - @Nonnull - private PhysicsOwnerResult runInline(@Nonnull PhysicsOwnerCommand command) - throws ExecutionException { - long sequence; - synchronized (lock) { - sequence = nextSequence++; - } - long startNanos = System.nanoTime(); - try { - PhysicsOwnerSnapshot snapshot = command.run(); - long completedNanos = System.nanoTime(); - return new PhysicsOwnerResult(sequence, - snapshot, - 0L, - completedNanos - startNanos, - completedNanos); - } catch (Throwable throwable) { - throw new ExecutionException(throwable); - } - } - - @Nonnull - private QueuedCommand enqueueLocked( - @Nonnull CommandKind kind, - @Nonnull PhysicsOwnerCommand command) { - requireAcceptingLocked(); - if (queuedCommandCountLocked() >= queueCapacity) { - throw new RejectedExecutionException("physics owner lane command queue is full"); - } - CompletableFuture future = new CompletableFuture<>(); - QueuedCommand queuedCommand = new QueuedCommand(nextSequence++, - command, - System.nanoTime(), - future); - if (kind == CommandKind.STEP) { - stepQueue.addLast(new QueuedStep(queuedCommand, queuedCommand.sequence() - 1L)); - } else { - mutationQueue.addLast(queuedCommand); - } - return queuedCommand; - } - - @Nullable - private QueuedCommand nextCommandLocked() { - activePreStepDrain = false; - QueuedStep queuedStep = stepQueue.peekFirst(); - if (queuedStep == null) { - return mutationQueue.pollFirst(); - } - QueuedCommand mutation = mutationQueue.peekFirst(); - if (mutation != null && mutation.sequence() <= queuedStep.drainBeforeSequence()) { - activePreStepDrain = true; - return mutationQueue.pollFirst(); - } - PendingStep currentStep = pendingStep; - if (currentStep != null) { - currentStep.recordLateMutationBacklog(mutationQueue.size()); - } - return stepQueue.pollFirst().command(); - } - - private void dispatchIfIdleLocked() { - if (!started || closed || active) { - return; - } - QueuedCommand next = nextCommandLocked(); - if (next == null) { - if (closing) { - closeIfReadyLocked(); - lock.notifyAll(); - } - return; - } - active = true; - activeCommand = next; - try { - scheduler.execute(this, () -> runQueuedCommand(next)); - } catch (RejectedExecutionException exception) { - active = false; - activePreStepDrain = false; - activeCommand = null; - completeCommandFuture(next, null, exception); - closeIfReadyLocked(); - if (closed) { - lock.notifyAll(); - return; - } - dispatchIfIdleLocked(); - } - } - - private void recordCompletionScheduled() { - synchronized (lock) { - activeCompletions++; - } - } - - private void recordCompletionFinished() { - synchronized (lock) { - if (activeCompletions <= 0) { - throw new IllegalStateException("physics owner lane completion count underflow"); - } - activeCompletions--; - closeIfReadyLocked(); - lock.notifyAll(); - } - } - - private void closeIfReadyLocked() { - if (!closing || closed || active || activeCompletions > 0 - || !mutationQueue.isEmpty() || !stepQueue.isEmpty()) { - return; - } - closed = true; - scheduler.unregister(this); - } - - private int queuedCommandCountLocked() { - return mutationQueue.size() + stepQueue.size(); - } - - private boolean activeCommandPendingLocked() { - if (!active) { - return false; - } - return activeCommand == null || !activeCommand.future().isDone(); - } - - private void requireAcceptingLocked() { - if (!started) { - throw new RejectedExecutionException("physics owner lane is not started"); - } - if (!accepting || closing || closed) { - throw new RejectedExecutionException("physics owner lane is closed"); - } - } - - private void rejectSynchronousCrossLaneWait() { - PhysicsOwnerLaneResource currentLane = scheduler.currentLane(); - if (currentLane != null && currentLane != this) { - throw new RejectedExecutionException( - "cannot synchronously wait for a different physics owner lane"); - } - } - - private void rejectSynchronousCompletionCallbackWait() { - if (scheduler.isCompletingAnyLane()) { - throw new RejectedExecutionException( - "cannot synchronously wait for a physics owner lane from a completion callback"); - } - } - - private void rejectSynchronousOwnerContextWait() { - if (isOwnerContext()) { - throw new RejectedExecutionException( - "cannot synchronously wait for a physics owner lane from its owner context"); - } - } - - @Nonnull - private static PhysicsOwnerMutationCompletion toMutationCompletion( - @Nonnull PendingMutation mutation) { - try { - return new PhysicsOwnerMutationCompletion(mutation.operation(), - mutation.future().get(), - null); - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - return new PhysicsOwnerMutationCompletion(mutation.operation(), null, exception); - } catch (ExecutionException exception) { - return new PhysicsOwnerMutationCompletion(mutation.operation(), - null, - exception.getCause()); - } - } - - private static void runDirect(@Nonnull String operation, - @Nonnull PhysicsOwnerMutation mutation) { - try { - mutation.run(); - } catch (RuntimeException exception) { - throw exception; - } catch (Exception exception) { - throw new IllegalStateException("Physics operation " + operation + " failed", - exception); - } - } - - @Nonnull - private static PhysicsMutationHandle runDirectAsync(@Nonnull String operation, - @Nullable T value, - @Nonnull PhysicsOwnerMutation mutation) { - try { - mutation.run(); - return PhysicsMutationHandle.completed(operation, value); - } catch (Throwable throwable) { - return PhysicsMutationHandle.failed(operation, value, throwable); - } - } - - @Nonnull - private static CompletableFuture callDirectAsync(@Nonnull PhysicsOwnerCallable callable) { - try { - return CompletableFuture.completedFuture(callable.call()); - } catch (Throwable throwable) { - CompletableFuture completion = new CompletableFuture<>(); - completion.completeExceptionally(throwable); - return completion; - } - } - - @Nonnull - private static T callDirect(@Nonnull String operation, - @Nonnull PhysicsOwnerCallable callable) { - try { - return callable.call(); - } catch (RuntimeException exception) { - throw exception; - } catch (Exception exception) { - throw new IllegalStateException("Physics operation " + operation + " failed", - exception); - } - } - - @Nonnull - private static RuntimeException ownerFailure(@Nonnull String operation, - @Nonnull Throwable cause) { - if (cause instanceof RuntimeException runtimeException) { - return runtimeException; - } - return new IllegalStateException(ownerFailureMessage(operation, cause), cause); - } - - @Nonnull - private static String ownerFailureMessage(@Nonnull String operation, - @Nonnull Throwable cause) { - StringBuilder message = new StringBuilder("Physics owner lane operation ") - .append(operation) - .append(" failed: ") - .append(cause.getClass().getSimpleName()); - String causeMessage = cause.getMessage(); - if (causeMessage != null && !causeMessage.isBlank()) { - message.append(": ").append(causeMessage); - } - return message.toString(); - } - - private enum CommandKind { - STEP, - MUTATION - } - - private record QueuedCommand(long sequence, - @Nonnull PhysicsOwnerCommand command, - long submittedNanos, - @Nonnull CompletableFuture future) { - } - - private record QueuedStep(@Nonnull QueuedCommand command, - long drainBeforeSequence) { - } - - private static final class PendingStep { - - @Nonnull - private final PhysicsOwnerStepCommand command; - @Nonnull - private final CompletableFuture future; - private final long submittedNanos; - private int preStepDrainedMutations; - private long preStepDrainRunNanos; - private int lateMutationBacklogAtStep; - - private PendingStep(@Nonnull PhysicsOwnerStepCommand command, - @Nonnull CompletableFuture future, - long submittedNanos) { - this.command = command; - this.future = future; - this.submittedNanos = submittedNanos; - } - - @Nonnull - private PhysicsOwnerStepCommand command() { - return command; - } - - @Nonnull - private CompletableFuture future() { - return future; - } - - private long submittedNanos() { - return submittedNanos; - } - - private int preStepDrainedMutations() { - return preStepDrainedMutations; - } - - private long preStepDrainRunNanos() { - return preStepDrainRunNanos; - } - - private int lateMutationBacklogAtStep() { - return lateMutationBacklogAtStep; - } - - private void recordPreStepDrain(long runNanos) { - preStepDrainedMutations++; - preStepDrainRunNanos += Math.max(0L, runNanos); - } - - private void recordLateMutationBacklog(int backlog) { - lateMutationBacklogAtStep = Math.max(0, backlog); - } - } - - private record PendingMutation(@Nonnull String operation, - @Nonnull CompletableFuture future) { - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerLaneScheduler.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerLaneScheduler.java deleted file mode 100644 index 4b4a5d87..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerLaneScheduler.java +++ /dev/null @@ -1,242 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Shared execution pool for serialized per-world physics owner lanes. - */ -public final class PhysicsOwnerLaneScheduler implements AutoCloseable { - - public static final int DEFAULT_POOL_SIZE = 1; - public static final int DEFAULT_QUEUE_CAPACITY = 128; - public static final Duration DEFAULT_CLOSE_TIMEOUT = Duration.ofSeconds(5L); - - private static final ThreadLocal CURRENT_LANE = - new ThreadLocal<>(); - private static final ThreadLocal COMPLETING_LANE = - new ThreadLocal<>(); - - private final Object lifecycleLock = new Object(); - private final Set lanes = ConcurrentHashMap.newKeySet(); - private final ExecutorService executor; - private final ExecutorService completionExecutor; - private final int queueCapacity; - @Nonnull - private final Duration closeTimeout; - private boolean closing; - private boolean closed; - - public PhysicsOwnerLaneScheduler(int poolSize, - int queueCapacity, - @Nonnull Duration closeTimeout) { - if (poolSize < 1) { - throw new IllegalArgumentException("poolSize must be positive"); - } - if (queueCapacity < 1) { - throw new IllegalArgumentException("queueCapacity must be positive"); - } - this.queueCapacity = queueCapacity; - this.closeTimeout = Objects.requireNonNull(closeTimeout, "closeTimeout"); - executor = Executors.newFixedThreadPool(poolSize, new OwnerLaneThreadFactory()); - completionExecutor = Executors.newThreadPerTaskExecutor( - Thread.ofVirtual().name("Impulse physics owner completion ", 1).factory()); - } - - @Nonnull - public PhysicsOwnerLaneResource createLane() { - synchronized (lifecycleLock) { - if (closing || closed) { - throw new RejectedExecutionException("physics owner lane scheduler is closed"); - } - PhysicsOwnerLaneResource lane = new PhysicsOwnerLaneResource(this, - queueCapacity, - closeTimeout); - lanes.add(lane); - return lane; - } - } - - boolean isCurrentLane(@Nonnull PhysicsOwnerLaneResource lane) { - return CURRENT_LANE.get() == lane; - } - - boolean isCompletingAnyLane() { - return COMPLETING_LANE.get() != null; - } - - @Nullable - PhysicsOwnerLaneResource currentLane() { - return CURRENT_LANE.get(); - } - - void execute(@Nonnull PhysicsOwnerLaneResource lane, - @Nonnull Runnable command) { - Objects.requireNonNull(lane, "lane"); - Objects.requireNonNull(command, "command"); - synchronized (lifecycleLock) { - if (closed || !lanes.contains(lane)) { - throw new RejectedExecutionException("physics owner lane is not registered"); - } - executor.execute(() -> runInLane(lane, command)); - } - } - - void completeOutsideOwnerContext(@Nonnull PhysicsOwnerLaneResource lane, - @Nonnull Runnable completion) { - Objects.requireNonNull(lane, "lane"); - Objects.requireNonNull(completion, "completion"); - try { - completionExecutor.execute(() -> runCompletion(lane, completion)); - } catch (RejectedExecutionException exception) { - try { - runCompletion(lane, completion); - } catch (RuntimeException | Error completionFailure) { - exception.addSuppressed(completionFailure); - } - } - } - - void unregister(@Nonnull PhysicsOwnerLaneResource lane) { - lanes.remove(Objects.requireNonNull(lane, "lane")); - } - - @Override - public void close() { - if (CURRENT_LANE.get() != null || COMPLETING_LANE.get() != null) { - throw new RejectedExecutionException( - "cannot synchronously close the physics owner lane scheduler from owner context"); - } - - ArrayList lanesToClose; - synchronized (lifecycleLock) { - if (closed) { - return; - } - closing = true; - lanesToClose = new ArrayList<>(lanes); - } - - RuntimeException closeFailure = null; - for (PhysicsOwnerLaneResource lane : lanesToClose) { - try { - lane.close(); - } catch (RuntimeException exception) { - if (closeFailure == null) { - closeFailure = exception; - } else { - closeFailure.addSuppressed(exception); - } - } - } - - executor.shutdown(); - boolean ownerStopped = awaitExecutorStop(executor); - completionExecutor.shutdown(); - boolean completionStopped = awaitExecutorStop(completionExecutor); - synchronized (lifecycleLock) { - closed = true; - } - if (!ownerStopped || !completionStopped) { - IllegalStateException timeout = new IllegalStateException( - "Physics owner lane scheduler did not stop within " + closeTimeout); - if (closeFailure != null) { - timeout.addSuppressed(closeFailure); - } - throw timeout; - } - if (closeFailure != null) { - throw closeFailure; - } - } - - private void runInLane(@Nonnull PhysicsOwnerLaneResource lane, - @Nonnull Runnable command) { - PhysicsOwnerLaneResource previous = CURRENT_LANE.get(); - CURRENT_LANE.set(lane); - try { - command.run(); - } finally { - if (previous == null) { - CURRENT_LANE.remove(); - } else { - CURRENT_LANE.set(previous); - } - } - } - - private void runCompletion(@Nonnull PhysicsOwnerLaneResource lane, - @Nonnull Runnable completion) { - PhysicsOwnerLaneResource previousCurrent = CURRENT_LANE.get(); - PhysicsOwnerLaneResource previousCompleting = COMPLETING_LANE.get(); - CURRENT_LANE.remove(); - COMPLETING_LANE.set(lane); - try { - completion.run(); - } finally { - if (previousCompleting == null) { - COMPLETING_LANE.remove(); - } else { - COMPLETING_LANE.set(previousCompleting); - } - if (previousCurrent == null) { - CURRENT_LANE.remove(); - } else { - CURRENT_LANE.set(previousCurrent); - } - } - } - - private boolean awaitExecutorStop(@Nonnull ExecutorService target) { - long timeoutNanos = Math.max(0L, closeTimeout.toNanos()); - long deadline = timeoutNanos == 0L - ? Long.MAX_VALUE - : System.nanoTime() + timeoutNanos; - boolean interrupted = false; - try { - while (!target.isTerminated()) { - long remainingNanos = deadline - System.nanoTime(); - if (remainingNanos <= 0L) { - return false; - } - try { - if (target.awaitTermination(remainingNanos, TimeUnit.NANOSECONDS)) { - return true; - } - } catch (InterruptedException exception) { - interrupted = true; - } - } - return true; - } finally { - if (interrupted) { - Thread.currentThread().interrupt(); - } - } - } - - private static final class OwnerLaneThreadFactory implements ThreadFactory { - - private final AtomicInteger nextThread = new AtomicInteger(1); - - @Override - public Thread newThread(@Nonnull Runnable runnable) { - Thread thread = new Thread(runnable, - "Impulse physics owner lane executor " + nextThread.getAndIncrement()); - thread.setDaemon(true); - return thread; - } - } - -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerMutation.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerMutation.java deleted file mode 100644 index b4653a44..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerMutation.java +++ /dev/null @@ -1,10 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -/** - * Internal mutation that runs in the current live-backend owner context. - */ -@FunctionalInterface -public interface PhysicsOwnerMutation { - - void run() throws Exception; -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerMutationCompletion.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerMutationCompletion.java deleted file mode 100644 index dc37c574..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerMutationCompletion.java +++ /dev/null @@ -1,27 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import java.util.Objects; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Completed non-step owner mutation consumed by the main tick after the future - * is already complete. - */ -public record PhysicsOwnerMutationCompletion(@Nonnull String operation, - @Nullable PhysicsOwnerResult result, - @Nullable Throwable executionFailure) { - - public PhysicsOwnerMutationCompletion { - Objects.requireNonNull(operation, "operation"); - } - - public boolean completedSuccessfully() { - return result != null && executionFailure == null; - } - - @Nonnull - public PhysicsOwnerSnapshot snapshotOrEmpty() { - return result != null ? result.snapshot() : PhysicsOwnerSnapshot.empty(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerResource.java deleted file mode 100644 index 163d16da..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerResource.java +++ /dev/null @@ -1,70 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import com.hypixel.hytale.component.Resource; -import com.hypixel.hytale.component.ResourceType; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Runtime ECS resource that owns serialized live-backend execution for one world. - */ -public interface PhysicsOwnerResource - extends Resource, AutoCloseable, PhysicsOwnerHandle { - - void start(@Nonnull String worldName); - - boolean isStarted(); - - boolean isClosed(); - - @Override - void close(); - - @Nonnull - PhysicsOwnerResult submitAndDrain(@Nonnull PhysicsOwnerCommand command) - throws InterruptedException, ExecutionException; - - boolean submitStepIfIdle(@Nonnull PhysicsOwnerCommand command); - - @Nonnull - PhysicsMutationHandle submitMutation(@Nonnull String operation, - @Nonnull PhysicsOwnerCommand command); - - @Nonnull - PhysicsMutationHandle submitMutation(@Nonnull String operation, - @Nullable T value, - @Nonnull PhysicsOwnerCommand command); - - @Nonnull - CompletableFuture submitMutationFuture(@Nonnull String operation, - @Nonnull PhysicsOwnerCommand command); - - @Nonnull - List pollCompletedMutations(int maxCompletions); - - @Nullable - PhysicsOwnerStepCompletion pollCompletedStep(); - - boolean hasPendingStep(); - - long pendingStepAgeNanos(); - - int pendingMutations(); - - int pendingCommands(); - - @Nonnull - @Override - PhysicsOwnerResource clone(); - - @Nonnull - static ResourceType getResourceType() { - return ImpulsePlugin.get().getPhysicsOwnerResourceType(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerResult.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerResult.java deleted file mode 100644 index f1b8bfdc..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerResult.java +++ /dev/null @@ -1,31 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Completed owner command metadata plus its published step snapshot. - */ -public record PhysicsOwnerResult(long sequence, - @Nonnull PhysicsOwnerSnapshot snapshot, - long queuedNanos, - long runNanos, - long completedNanos) { - - public PhysicsOwnerResult { - if (sequence < 1L) { - throw new IllegalArgumentException("sequence must be positive"); - } - Objects.requireNonNull(snapshot, "snapshot"); - queuedNanos = Math.max(0L, queuedNanos); - runNanos = Math.max(0L, runNanos); - completedNanos = Math.max(0L, completedNanos); - } - - public PhysicsOwnerResult(long sequence, - @Nonnull PhysicsOwnerSnapshot snapshot, - long queuedNanos, - long runNanos) { - this(sequence, snapshot, queuedNanos, runNanos, 0L); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerSnapshot.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerSnapshot.java deleted file mode 100644 index 57145e39..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerSnapshot.java +++ /dev/null @@ -1,51 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import dev.hytalemodding.impulse.api.PhysicsStepPhaseStats; -import javax.annotation.Nonnull; - -/** - * Step output published by a physics owner command. - */ -public record PhysicsOwnerSnapshot(int spaces, - int substeps, - int bodySnapshots, - int spatialIndexCells, - long stepNanos, - long snapshotNanos, - @Nonnull PhysicsStepPhaseStats nativePhaseStats) { - - public PhysicsOwnerSnapshot { - spaces = Math.max(0, spaces); - substeps = Math.max(0, substeps); - bodySnapshots = Math.max(0, bodySnapshots); - spatialIndexCells = Math.max(0, spatialIndexCells); - stepNanos = Math.max(0L, stepNanos); - snapshotNanos = Math.max(0L, snapshotNanos); - } - - public PhysicsOwnerSnapshot(int spaces, - int substeps, - int bodySnapshots, - int spatialIndexCells, - long stepNanos, - long snapshotNanos) { - this(spaces, - substeps, - bodySnapshots, - spatialIndexCells, - stepNanos, - snapshotNanos, - PhysicsStepPhaseStats.unavailable()); - } - - @Nonnull - public static PhysicsOwnerSnapshot empty() { - return new PhysicsOwnerSnapshot(0, - 0, - 0, - 0, - 0L, - 0L, - PhysicsStepPhaseStats.unavailable()); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerStepCommand.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerStepCommand.java deleted file mode 100644 index d4326481..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerStepCommand.java +++ /dev/null @@ -1,506 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsContactPhase; -import dev.hytalemodding.impulse.api.PhysicsStepPhaseStats; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.runtime.BackendStepPhaseStatsSink; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistration; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodySnapshots; -import dev.hytalemodding.impulse.core.internal.systems.step.PhysicsStepCountPolicy; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsContactEvent; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsFrameEvent; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsEventCollectionMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import java.util.ArrayList; -import java.util.Objects; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3f; - -/** - * Owner-lane execution of the world physics step. - * - *

Runtime mutations are owner-lane operations. Callers must - * only submit this command when the lane is the exclusive owner of backend - * spaces and the resource until the result has been consumed.

- * - *

{@code stepSequence} and {@code serverTick} are copied into the published - * snapshot frame for diagnostics and external correlation. They do not control - * publication freshness; that remains guarded by the resource's frame and world - * epochs.

- */ -public final class PhysicsOwnerStepCommand implements PhysicsOwnerCommand { - - private static final float DEFAULT_LINEAR_TRAVEL_PER_SUBSTEP = 0.45f; - private static final float MIN_LINEAR_TRAVEL_PER_SUBSTEP = 0.125f; - private static final float SHAPE_TRAVEL_FRACTION = 0.75f; - private static final float MAX_ANGULAR_RADIANS_PER_SUBSTEP = (float) Math.toRadians(30.0); - - @Nonnull - private final PhysicsWorldRuntimeResource resource; - private final float dt; - private final boolean profilingEnabled; - private final long stepSequence; - private final long serverTick; - @Nullable - private RuntimeException failure; - @Nullable - private PublishedPhysicsSnapshotFrame publishedFrame; - @Nullable - private PhysicsEventFrame eventFrame; - - public PhysicsOwnerStepCommand(@Nonnull PhysicsWorldResource resource, - float dt, - boolean profilingEnabled) { - this(resource, dt, profilingEnabled, 0L, 0L); - } - - /** - * Creates an owner step command with snapshot correlation metadata. - * - * @param stepSequence monotonic Impulse step-scheduler sequence; not a - * Hytale world tick and not guaranteed contiguous in published frames - * @param serverTick Hytale world tick observed when the step command was - * scheduled; not a physics step counter - */ - public PhysicsOwnerStepCommand(@Nonnull PhysicsWorldResource resource, - float dt, - boolean profilingEnabled, - long stepSequence, - long serverTick) { - this.resource = PhysicsWorldRuntimeResource.require( - Objects.requireNonNull(resource, "resource")); - this.dt = dt; - this.profilingEnabled = profilingEnabled; - this.stepSequence = Math.max(0L, stepSequence); - this.serverTick = Math.max(0L, serverTick); - } - - @Nonnull - @Override - public PhysicsOwnerSnapshot run() { - StepExecution result = runStepCycle(resource, - dt, - profilingEnabled, - stepSequence, - serverTick); - failure = result.failure(); - publishedFrame = result.frame(); - eventFrame = result.eventFrame(); - return result.snapshot(); - } - - @Nullable - public RuntimeException failure() { - return failure; - } - - @Nullable - public PublishedPhysicsSnapshotFrame publishedFrame() { - return publishedFrame; - } - - @Nullable - public PhysicsEventFrame eventFrame() { - return eventFrame; - } - - @Nonnull - static PhysicsOwnerSnapshot runStep(@Nonnull PhysicsWorldResource resource, - float dt, - boolean profilingEnabled) { - return runStepCycle(resource, dt, profilingEnabled, 0L, 0L).snapshot(); - } - - @Nonnull - static StepExecution runStepCycle(@Nonnull PhysicsWorldResource resource, - float dt, - boolean profilingEnabled, - long stepSequence, - long serverTick) { - PhysicsWorldRuntimeResource runtime = PhysicsWorldRuntimeResource.require( - Objects.requireNonNull(resource, "resource")); - float safeDt = Float.isFinite(dt) ? Math.max(dt, 0.0f) : 0.0f; - PhysicsWorldSettings settings = runtime.getWorldSettings(); - PhysicsStepMode stepMode = settings.getStepMode(); - PhysicsEventCollectionMode eventCollectionMode = settings.getEventCollectionMode(); - int simulationSteps = settings.getSimulationSteps(); - float configuredMaxStepDt = settings.getMaxStepDt(); - float maxStepDt = configuredMaxStepDt > 0f - ? configuredMaxStepDt - : PhysicsWorldSettings.DEFAULT_MAX_STEP_DT; - int steps = stepMode == PhysicsStepMode.ADAPTIVE - ? resolveAdaptiveStepCount(safeDt, simulationSteps, maxStepDt, runtime) - : PhysicsStepCountPolicy.resolveStepCount(safeDt, - simulationSteps, - maxStepDt, - stepMode); - - long startNanos = profilingEnabled ? System.nanoTime() : 0L; - StepCounters counters = new StepCounters(); - StepBackendEvents backendEvents = new StepBackendEvents(); - RuntimeException stepFailure = null; - try { - if (profilingEnabled) { - resetStepPhaseStats(runtime); - } - if (stepMode != PhysicsStepMode.CCD) { - restoreForcedContinuousCollision(runtime); - } - executeSteps(runtime, - safeDt, - stepMode, - steps, - counters, - backendEvents, - eventCollectionMode.collectsBackendEvents()); - } catch (RuntimeException exception) { - stepFailure = exception; - } - long stepNanos = profilingEnabled ? System.nanoTime() - startNanos : 0L; - - PublishedPhysicsSnapshotFrame frame; - try { - frame = runtime.capturePublishedSnapshotFrame(stepSequence, - serverTick, - stepFailure == null - ? PublishedPhysicsSnapshotFrame.Status.COMPLETE - : PublishedPhysicsSnapshotFrame.Status.PARTIAL, - stepNanos, - profilingEnabled, - backendEvents.physicsEvents, - backendEvents.droppedBackendEventCount); - } catch (RuntimeException exception) { - if (stepFailure != null) { - stepFailure.addSuppressed(exception); - throw stepFailure; - } - throw exception; - } - PhysicsEventFrame eventFrame = runtime.getLatestEventFrame(); - - PhysicsStepPhaseStats nativePhaseStats = profilingEnabled - ? collectStepPhaseStats(runtime) - : PhysicsStepPhaseStats.unavailable(); - return new StepExecution(new PhysicsOwnerSnapshot(counters.spaceCount(), - counters.substeps(), - frame.bodyCount(), - frame.spatialIndexCellCount(), - stepNanos, - frame.snapshotNanos(), - nativePhaseStats), stepFailure, frame, eventFrame); - } - - private static void resetStepPhaseStats(@Nonnull PhysicsWorldRuntimeResource resource) { - for (PhysicsSpaceBinding space : resource.iterateSpaceBindings()) { - space.runtime().resetStepPhaseStats(space.backendSpaceHandle().value()); - } - } - - @Nonnull - private static PhysicsStepPhaseStats collectStepPhaseStats( - @Nonnull PhysicsWorldRuntimeResource resource) { - PhysicsStepPhaseStats stats = PhysicsStepPhaseStats.unavailable(); - for (PhysicsSpaceBinding space : resource.iterateSpaceBindings()) { - StepPhaseStatsCapture spaceStats = new StepPhaseStatsCapture(); - space.runtime().stepPhaseStats(space.backendSpaceHandle().value(), - spaceStats); - stats = stats.add(spaceStats.value()); - } - return stats; - } - - private static final class StepPhaseStatsCapture implements BackendStepPhaseStatsSink { - - @Nonnull - private PhysicsStepPhaseStats value = PhysicsStepPhaseStats.unavailable(); - - @Override - public void accept(long stepNanos, - long broadPhaseNanos, - long narrowPhaseNanos, - long solverNanos, - long continuousCollisionNanos, - long snapshotNanos, - boolean available) { - value = available - ? PhysicsStepPhaseStats.available(stepNanos, - broadPhaseNanos, - narrowPhaseNanos, - solverNanos, - continuousCollisionNanos, - snapshotNanos) - : PhysicsStepPhaseStats.unavailable(); - } - - @Nonnull - private PhysicsStepPhaseStats value() { - return value; - } - } - - private static int resolveAdaptiveStepCount(float dt, - int simulationSteps, - float maxStepDt, - @Nonnull PhysicsWorldRuntimeResource resource) { - float sampledDt = Math.max(dt, 0.0f); - if (sampledDt <= 0.0f) { - return simulationSteps; - } - - int minimumSteps = PhysicsStepCountPolicy.resolveMaxStepCount(sampledDt, - simulationSteps, - maxStepDt); - StepRisk risk = new StepRisk(sampledDt, minimumSteps); - for (PhysicsBodyRegistration registration : resource.getBodyRegistrations()) { - PhysicsSpaceBinding space = resource.getSpaceBinding(registration.spaceId()); - if (space == null) { - continue; - } - PhysicsBodySnapshot snapshot = PhysicsBodySnapshots.read(space, registration.backendBodyHandle().value()); - if (snapshot != null) { - risk.inspect(snapshot); - } - } - return risk.steps(); - } - - private static void executeSteps(@Nonnull PhysicsWorldRuntimeResource resource, - float safeDt, - @Nonnull PhysicsStepMode stepMode, - int steps, - @Nonnull StepCounters counters, - @Nonnull StepBackendEvents backendEvents, - boolean collectBackendEvents) { - float stepDt = safeDt / steps; - for (PhysicsSpaceBinding space : resource.iterateSpaceBindings()) { - counters.spaceCount++; - if (stepMode == PhysicsStepMode.CCD && supportsContinuousCollision(space)) { - forceContinuousCollision(resource, space); - } - for (int step = 0; step < steps; step++) { - space.runtime().step(space.backendSpaceHandle().value(), stepDt); - counters.substeps++; - if (collectBackendEvents) { - collectContactEvents(resource, space, backendEvents); - } - } - } - } - - private static void collectContactEvents(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull PhysicsSpaceBinding space, - @Nonnull StepBackendEvents backendEvents) { - space.runtime().contacts(space.backendSpaceHandle().value(), (bodyAId, - bodyBId, - pointAX, - pointAY, - pointAZ, - pointBX, - pointBY, - pointBZ, - normalBX, - normalBY, - normalBZ, - distance, - impulse) -> { - RigidBodyKey bodyAKey = resource.getBodyKey(space.spaceId(), bodyAId); - RigidBodyKey bodyBKey = resource.getBodyKey(space.spaceId(), bodyBId); - if (bodyAKey == null || bodyBKey == null) { - backendEvents.droppedBackendEventCount++; - return; - } - backendEvents.physicsEvents.add(new PhysicsContactEvent(space.spaceId(), - PhysicsContactPhase.OBSERVED, - bodyAKey, - bodyBKey, - new Vector3f(pointAX, pointAY, pointAZ), - new Vector3f(pointBX, pointBY, pointBZ), - new Vector3f(normalBX, normalBY, normalBZ), - distance, - impulse)); - }); - } - - private static boolean supportsContinuousCollision(@Nonnull PhysicsSpaceBinding space) { - return space.runtime().supportsContinuousCollision(space.backendSpaceHandle().value()); - } - - private static int requiredSteps(float travel, float safeTravel) { - if (travel <= safeTravel) { - return 1; - } - return (int) Math.ceil(travel / safeTravel); - } - - private static float safeLinearTravel(@Nonnull PhysicsBodySnapshot body) { - return Math.clamp( - approximateMinimumExtent(body) * SHAPE_TRAVEL_FRACTION, - MIN_LINEAR_TRAVEL_PER_SUBSTEP, - DEFAULT_LINEAR_TRAVEL_PER_SUBSTEP); - } - - private static float safeAngularTravel(@Nonnull PhysicsBodySnapshot body) { - return Math.clamp( - approximateShapeRadius(body) * MAX_ANGULAR_RADIANS_PER_SUBSTEP, - MIN_LINEAR_TRAVEL_PER_SUBSTEP, - DEFAULT_LINEAR_TRAVEL_PER_SUBSTEP); - } - - private static float approximateMinimumExtent(@Nonnull PhysicsBodySnapshot body) { - ShapeType shapeType = body.shapeType(); - if (shapeType == ShapeType.BOX) { - Vector3f halfExtents = body.boxHalfExtents(); - if (halfExtents != null) { - return Math.max(MIN_LINEAR_TRAVEL_PER_SUBSTEP, - Math.min(halfExtents.x, Math.min(halfExtents.y, halfExtents.z))); - } - } - if (shapeType == ShapeType.SPHERE) { - return Math.max(MIN_LINEAR_TRAVEL_PER_SUBSTEP, body.sphereRadius()); - } - if (shapeType == ShapeType.CAPSULE - || shapeType == ShapeType.CYLINDER - || shapeType == ShapeType.CONE) { - return Math.max(MIN_LINEAR_TRAVEL_PER_SUBSTEP, body.sphereRadius()); - } - return DEFAULT_LINEAR_TRAVEL_PER_SUBSTEP; - } - - private static float approximateShapeRadius(@Nonnull PhysicsBodySnapshot body) { - ShapeType shapeType = body.shapeType(); - if (shapeType == ShapeType.BOX) { - Vector3f halfExtents = body.boxHalfExtents(); - if (halfExtents != null) { - return Math.max(MIN_LINEAR_TRAVEL_PER_SUBSTEP, - (float) Math.sqrt(halfExtents.x * halfExtents.x - + halfExtents.y * halfExtents.y - + halfExtents.z * halfExtents.z)); - } - } - if (shapeType == ShapeType.SPHERE) { - return Math.max(MIN_LINEAR_TRAVEL_PER_SUBSTEP, body.sphereRadius()); - } - if (shapeType == ShapeType.CAPSULE - || shapeType == ShapeType.CYLINDER - || shapeType == ShapeType.CONE) { - return Math.max(MIN_LINEAR_TRAVEL_PER_SUBSTEP, - body.sphereRadius() + body.halfHeight()); - } - return DEFAULT_LINEAR_TRAVEL_PER_SUBSTEP; - } - - private static void forceContinuousCollision(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull PhysicsSpaceBinding space) { - for (PhysicsBodyRegistration registration : resource.getBodyRegistrations()) { - if (!registration.spaceId().equals(space.spaceId())) { - continue; - } - PhysicsBodySnapshot snapshot = PhysicsBodySnapshots.read(space, registration.backendBodyHandle().value()); - if (snapshot == null - || !snapshot.isDynamic() - || space.runtime().isBodyContinuousCollisionEnabled(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value())) { - continue; - } - space.runtime().setBodyContinuousCollision(space.backendSpaceHandle().value(), registration.backendBodyHandle().value(), true); - resource.markContinuousCollisionForced(registration.bodyKey()); - } - } - - private static void restoreForcedContinuousCollision(@Nonnull PhysicsWorldRuntimeResource resource) { - if (!resource.hasForcedContinuousCollisionBodies()) { - return; - } - - resource.forEachForcedContinuousCollisionBody(bodyKey -> { - PhysicsBodyRegistration registration = resource.getRegistration(bodyKey); - if (registration != null) { - PhysicsSpaceBinding space = resource.getSpaceBinding(registration.spaceId()); - if (space != null) { - space.runtime() - .setBodyContinuousCollision(space.backendSpaceHandle().value(), registration.backendBodyHandle().value(), false); - } - } - }); - resource.clearForcedContinuousCollisionBodies(); - } - - private static final class StepRisk { - - private final float dt; - private int steps; - @Nonnull - private final Vector3f linearVelocity = new Vector3f(); - @Nonnull - private final Vector3f angularVelocity = new Vector3f(); - - private StepRisk(float dt, int minimumSteps) { - this.dt = dt; - steps = minimumSteps; - } - - private void inspect(@Nonnull PhysicsBodySnapshot body) { - if (steps >= PhysicsWorldSettings.MAX_SIMULATION_STEPS - || body.isStatic() - || body.sleeping() - || body.sensor() - || (!body.isDynamic() && !body.isKinematic())) { - return; - } - - body.copyLinearVelocityTo(linearVelocity); - body.copyAngularVelocityTo(angularVelocity); - float linearTravel = linearVelocity.length() * dt; - float angularSurfaceTravel = angularVelocity.length() - * approximateShapeRadius(body) - * dt; - float safeLinearTravel = safeLinearTravel(body); - int requiredSteps = Math.max( - requiredSteps(linearTravel, safeLinearTravel), - requiredSteps(angularSurfaceTravel, safeAngularTravel(body))); - steps = Math.clamp(steps, - Math.min(requiredSteps, PhysicsWorldSettings.MAX_SIMULATION_STEPS), - PhysicsWorldSettings.MAX_SIMULATION_STEPS); - } - - private int steps() { - return steps; - } - } - - record StepExecution(@Nonnull PhysicsOwnerSnapshot snapshot, - @Nullable RuntimeException failure, - @Nonnull PublishedPhysicsSnapshotFrame frame, - @Nonnull PhysicsEventFrame eventFrame) { - } - - private static final class StepBackendEvents { - - @Nonnull - private final ArrayList physicsEvents = new ArrayList<>(); - private int droppedBackendEventCount; - } - - private static final class StepCounters { - - private int spaceCount; - private int substeps; - - private int spaceCount() { - return spaceCount; - } - - private int substeps() { - return substeps; - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerStepCompletion.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerStepCompletion.java deleted file mode 100644 index 4c10aa24..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerStepCompletion.java +++ /dev/null @@ -1,43 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Non-blocking step completion consumed by the main tick once the owner future - * has already completed. - */ -public record PhysicsOwnerStepCompletion(@Nullable PhysicsOwnerResult result, - @Nullable PublishedPhysicsSnapshotFrame frame, - @Nullable PhysicsEventFrame eventFrame, - @Nullable RuntimeException stepFailure, - int preStepDrainedMutations, - long preStepDrainRunNanos, - int lateMutationBacklogAtStep, - @Nullable Throwable executionFailure) { - - public PhysicsOwnerStepCompletion(@Nullable PhysicsOwnerResult result, - @Nullable PublishedPhysicsSnapshotFrame frame, - @Nullable PhysicsEventFrame eventFrame, - @Nullable RuntimeException stepFailure, - @Nullable Throwable executionFailure) { - this(result, frame, eventFrame, stepFailure, 0, 0L, 0, executionFailure); - } - - public PhysicsOwnerStepCompletion { - preStepDrainedMutations = Math.max(0, preStepDrainedMutations); - preStepDrainRunNanos = Math.max(0L, preStepDrainRunNanos); - lateMutationBacklogAtStep = Math.max(0, lateMutationBacklogAtStep); - } - - public boolean completedSuccessfully() { - return result != null && executionFailure == null; - } - - @Nonnull - public PhysicsOwnerSnapshot snapshotOrEmpty() { - return result != null ? result.snapshot() : PhysicsOwnerSnapshot.empty(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/profiling/PhysicsRuntimeProfilingResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/profiling/PhysicsRuntimeProfilingResource.java index 23392a2e..cfef6ed3 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/profiling/PhysicsRuntimeProfilingResource.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/resources/profiling/PhysicsRuntimeProfilingResource.java @@ -4,7 +4,6 @@ import com.hypixel.hytale.component.ResourceType; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import dev.hytalemodding.impulse.api.PhysicsStepPhaseStats; -import dev.hytalemodding.impulse.core.ImpulsePlugin; import javax.annotation.Nonnull; import javax.annotation.Nullable; import lombok.Getter; @@ -19,6 +18,9 @@ */ public class PhysicsRuntimeProfilingResource implements Resource { + @Nullable + private static ResourceType resourceType; + private boolean enabled; private final StepSnapshot cumulativeStep = new StepSnapshot(); @@ -34,7 +36,7 @@ public class PhysicsRuntimeProfilingResource implements Resource { @Nullable private transient SyncCollector activeSyncCollector; - private transient long previousOwnerStepCompletedNanos; + private transient long previousStoreTickStepCompletedNanos; public PhysicsRuntimeProfilingResource() { } @@ -128,16 +130,16 @@ public synchronized void recordStep(int spaces, int bodySnapshots, int spatialIndexCells, long snapshotNanos, - long ownerQueuedNanos, - long ownerRunNanos) { + long storeTickQueuedNanos, + long storeTickRunNanos) { recordStep(spaces, substeps, nanos, bodySnapshots, spatialIndexCells, snapshotNanos, - ownerQueuedNanos, - ownerRunNanos, + storeTickQueuedNanos, + storeTickRunNanos, 0L, PhysicsStepPhaseStats.unavailable()); } @@ -148,8 +150,8 @@ public synchronized void recordStep(int spaces, int bodySnapshots, int spatialIndexCells, long snapshotNanos, - long ownerQueuedNanos, - long ownerRunNanos, + long storeTickQueuedNanos, + long storeTickRunNanos, @Nonnull PhysicsStepPhaseStats nativePhaseStats) { recordStep(spaces, substeps, @@ -157,8 +159,8 @@ public synchronized void recordStep(int spaces, bodySnapshots, spatialIndexCells, snapshotNanos, - ownerQueuedNanos, - ownerRunNanos, + storeTickQueuedNanos, + storeTickRunNanos, 0L, nativePhaseStats); } @@ -169,9 +171,9 @@ public synchronized void recordStep(int spaces, int bodySnapshots, int spatialIndexCells, long snapshotNanos, - long ownerQueuedNanos, - long ownerRunNanos, - long ownerCompletedNanos, + long storeTickQueuedNanos, + long storeTickRunNanos, + long storeTickCompletedNanos, @Nonnull PhysicsStepPhaseStats nativePhaseStats) { recordStep(spaces, substeps, @@ -179,9 +181,9 @@ public synchronized void recordStep(int spaces, bodySnapshots, spatialIndexCells, snapshotNanos, - ownerQueuedNanos, - ownerRunNanos, - ownerCompletedNanos, + storeTickQueuedNanos, + storeTickRunNanos, + storeTickCompletedNanos, nativePhaseStats, 0, 0L, @@ -194,9 +196,9 @@ public synchronized void recordStep(int spaces, int bodySnapshots, int spatialIndexCells, long snapshotNanos, - long ownerQueuedNanos, - long ownerRunNanos, - long ownerCompletedNanos, + long storeTickQueuedNanos, + long storeTickRunNanos, + long storeTickCompletedNanos, @Nonnull PhysicsStepPhaseStats nativePhaseStats, int preStepDrainedMutations, long preStepDrainRunNanos, @@ -209,9 +211,9 @@ public synchronized void recordStep(int spaces, snapshot.setBodySnapshots(bodySnapshots); snapshot.setSpatialIndexCells(spatialIndexCells); snapshot.setSnapshotNanos(snapshotNanos); - snapshot.setOwnerQueuedNanos(ownerQueuedNanos); - snapshot.setOwnerRunNanos(ownerRunNanos); - snapshot.recordOwnerStepInterval(recordOwnerStepInterval(ownerCompletedNanos)); + snapshot.setStoreTickQueuedNanos(storeTickQueuedNanos); + snapshot.setStoreTickRunNanos(storeTickRunNanos); + snapshot.recordStoreTickStepInterval(recordStoreTickStepInterval(storeTickCompletedNanos)); snapshot.setNativePhaseStats(nativePhaseStats); snapshot.recordPreStepDrain(Math.max(0, preStepDrainedMutations), Math.max(0L, preStepDrainRunNanos), @@ -317,7 +319,7 @@ public synchronized void reset() { latestVisual.reset(); worstVisual.reset(); activeSyncCollector = null; - previousOwnerStepCompletedNanos = 0L; + previousStoreTickStepCompletedNanos = 0L; } @Nonnull @@ -335,23 +337,23 @@ public synchronized PhysicsRuntimeProfilingResource clone() { copy.cumulativeVisual.copyFrom(cumulativeVisual); copy.latestVisual.copyFrom(latestVisual); copy.worstVisual.copyFrom(worstVisual); - copy.previousOwnerStepCompletedNanos = previousOwnerStepCompletedNanos; + copy.previousStoreTickStepCompletedNanos = previousStoreTickStepCompletedNanos; return copy; } - private long recordOwnerStepInterval(long ownerCompletedNanos) { - if (ownerCompletedNanos <= 0L) { + private long recordStoreTickStepInterval(long storeTickCompletedNanos) { + if (storeTickCompletedNanos <= 0L) { return 0L; } - if (previousOwnerStepCompletedNanos <= 0L) { - previousOwnerStepCompletedNanos = ownerCompletedNanos; + if (previousStoreTickStepCompletedNanos <= 0L) { + previousStoreTickStepCompletedNanos = storeTickCompletedNanos; return 0L; } - if (ownerCompletedNanos <= previousOwnerStepCompletedNanos) { + if (storeTickCompletedNanos <= previousStoreTickStepCompletedNanos) { return 0L; } - long intervalNanos = ownerCompletedNanos - previousOwnerStepCompletedNanos; - previousOwnerStepCompletedNanos = ownerCompletedNanos; + long intervalNanos = storeTickCompletedNanos - previousStoreTickStepCompletedNanos; + previousStoreTickStepCompletedNanos = storeTickCompletedNanos; return intervalNanos; } @@ -367,7 +369,16 @@ private static long secondsToNanos(float seconds) { } public static ResourceType getResourceType() { - return ImpulsePlugin.get().getPhysicsRuntimeProfilingResourceType(); + return resourceType; + } + + public static void setResourceType( + @Nonnull ResourceType type) { + resourceType = type; + } + + public static void clearResourceType() { + resourceType = null; } @Nonnull @@ -408,17 +419,17 @@ public static final class StepSnapshot { @Setter private long snapshotNanos; @Setter - private long ownerQueuedNanos; + private long storeTickQueuedNanos; @Setter - private long ownerRunNanos; + private long storeTickRunNanos; private int preStepDrainedMutations; private int maxPreStepDrainedMutations; private long preStepDrainRunNanos; private int lateMutationBacklogAtStep; private int maxLateMutationBacklogAtStep; - private int ownerStepRateSamples; - private long ownerStepIntervalNanos; - private long maxOwnerStepIntervalNanos; + private int storeTickStepRateSamples; + private long storeTickStepIntervalNanos; + private long maxStoreTickStepIntervalNanos; @Setter private int skippedPendingSteps; @Setter @@ -454,16 +465,16 @@ public void copyFrom(@Nonnull StepSnapshot other) { spatialIndexCells = other.spatialIndexCells; tickNanos = other.tickNanos; snapshotNanos = other.snapshotNanos; - ownerQueuedNanos = other.ownerQueuedNanos; - ownerRunNanos = other.ownerRunNanos; + storeTickQueuedNanos = other.storeTickQueuedNanos; + storeTickRunNanos = other.storeTickRunNanos; preStepDrainedMutations = other.preStepDrainedMutations; maxPreStepDrainedMutations = other.maxPreStepDrainedMutations; preStepDrainRunNanos = other.preStepDrainRunNanos; lateMutationBacklogAtStep = other.lateMutationBacklogAtStep; maxLateMutationBacklogAtStep = other.maxLateMutationBacklogAtStep; - ownerStepRateSamples = other.ownerStepRateSamples; - ownerStepIntervalNanos = other.ownerStepIntervalNanos; - maxOwnerStepIntervalNanos = other.maxOwnerStepIntervalNanos; + storeTickStepRateSamples = other.storeTickStepRateSamples; + storeTickStepIntervalNanos = other.storeTickStepIntervalNanos; + maxStoreTickStepIntervalNanos = other.maxStoreTickStepIntervalNanos; skippedPendingSteps = other.skippedPendingSteps; pendingStepAgeNanos = other.pendingStepAgeNanos; maxPendingStepAgeNanos = other.maxPendingStepAgeNanos; @@ -492,8 +503,8 @@ public void add(@Nonnull StepSnapshot other) { spatialIndexCells += other.spatialIndexCells; tickNanos += other.tickNanos; snapshotNanos += other.snapshotNanos; - ownerQueuedNanos += other.ownerQueuedNanos; - ownerRunNanos += other.ownerRunNanos; + storeTickQueuedNanos += other.storeTickQueuedNanos; + storeTickRunNanos += other.storeTickRunNanos; preStepDrainedMutations += other.preStepDrainedMutations; maxPreStepDrainedMutations = Math.max(maxPreStepDrainedMutations, other.maxPreStepDrainedMutations); @@ -501,10 +512,10 @@ public void add(@Nonnull StepSnapshot other) { lateMutationBacklogAtStep += other.lateMutationBacklogAtStep; maxLateMutationBacklogAtStep = Math.max(maxLateMutationBacklogAtStep, other.maxLateMutationBacklogAtStep); - ownerStepRateSamples += other.ownerStepRateSamples; - ownerStepIntervalNanos += other.ownerStepIntervalNanos; - maxOwnerStepIntervalNanos = Math.max(maxOwnerStepIntervalNanos, - other.maxOwnerStepIntervalNanos); + storeTickStepRateSamples += other.storeTickStepRateSamples; + storeTickStepIntervalNanos += other.storeTickStepIntervalNanos; + maxStoreTickStepIntervalNanos = Math.max(maxStoreTickStepIntervalNanos, + other.maxStoreTickStepIntervalNanos); skippedPendingSteps += other.skippedPendingSteps; pendingStepAgeNanos += other.pendingStepAgeNanos; maxPendingStepAgeNanos = Math.max(maxPendingStepAgeNanos, @@ -535,16 +546,16 @@ public void reset() { spatialIndexCells = 0; tickNanos = 0L; snapshotNanos = 0L; - ownerQueuedNanos = 0L; - ownerRunNanos = 0L; + storeTickQueuedNanos = 0L; + storeTickRunNanos = 0L; preStepDrainedMutations = 0; maxPreStepDrainedMutations = 0; preStepDrainRunNanos = 0L; lateMutationBacklogAtStep = 0; maxLateMutationBacklogAtStep = 0; - ownerStepRateSamples = 0; - ownerStepIntervalNanos = 0L; - maxOwnerStepIntervalNanos = 0L; + storeTickStepRateSamples = 0; + storeTickStepIntervalNanos = 0L; + maxStoreTickStepIntervalNanos = 0L; skippedPendingSteps = 0; pendingStepAgeNanos = 0L; maxPendingStepAgeNanos = 0L; @@ -565,16 +576,16 @@ public void reset() { nativeSnapshotNanos = 0L; } - public void recordOwnerStepInterval(long intervalNanos) { + public void recordStoreTickStepInterval(long intervalNanos) { if (intervalNanos <= 0L) { - ownerStepRateSamples = 0; - ownerStepIntervalNanos = 0L; - maxOwnerStepIntervalNanos = 0L; + storeTickStepRateSamples = 0; + storeTickStepIntervalNanos = 0L; + maxStoreTickStepIntervalNanos = 0L; return; } - ownerStepRateSamples = 1; - ownerStepIntervalNanos = intervalNanos; - maxOwnerStepIntervalNanos = intervalNanos; + storeTickStepRateSamples = 1; + storeTickStepIntervalNanos = intervalNanos; + maxStoreTickStepIntervalNanos = intervalNanos; } public void recordPreStepDrain(int drainedMutations, diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsCommandDispatcher.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsCommandDispatcher.java deleted file mode 100644 index 36a85be9..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsCommandDispatcher.java +++ /dev/null @@ -1,174 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.simulation.JointType; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Internal execution target for frozen physics command batches. - */ -public interface PhysicsCommandDispatcher { - - void spawnRigidBody(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - float positionX, - float positionY, - float positionZ, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode); - - default void spawnRigidBodies(int bodyCount, - @Nonnull RigidBodyKey[] bodyKeys, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - @Nonnull float[] positions, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - for (int index = 0; index < bodyCount; index++) { - int positionOffset = index * 3; - spawnRigidBody(bodyKeys[index], - spaceId, - shape, - mass, - bodyType, - positions[positionOffset], - positions[positionOffset + 1], - positions[positionOffset + 2], - settings, - kind, - persistenceMode); - } - } - - default void spawnRigidBodies(int bodyCount, - @Nonnull long[] bodyKeyMostSignificantBits, - @Nonnull long[] bodyKeyLeastSignificantBits, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - @Nonnull float[] positions, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - for (int index = 0; index < bodyCount; index++) { - int positionOffset = index * 3; - RigidBodyKey bodyKey = RigidBodyKey.of(bodyKeyMostSignificantBits[index], - bodyKeyLeastSignificantBits[index]); - spawnRigidBody(bodyKey, - spaceId, - shape, - mass, - bodyType, - positions[positionOffset], - positions[positionOffset + 1], - positions[positionOffset + 2], - settings, - kind, - persistenceMode); - } - } - - void destroyRigidBody(@Nonnull RigidBodyKey bodyKey); - - void setSpaceGravity(@Nonnull SpaceId spaceId, - float x, - float y, - float z); - - void setRigidBodyTransform(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ, - float rotationX, - float rotationY, - float rotationZ, - float rotationW, - boolean activate); - - void setRigidBodyPosition(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ, - boolean activate); - - void setRigidBodyVelocity(@Nonnull RigidBodyKey bodyKey, - float linearX, - float linearY, - float linearZ, - float angularX, - float angularY, - float angularZ, - boolean activate); - - void setRigidBodyType(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodyType bodyType, - boolean activate); - - void activateRigidBody(@Nonnull RigidBodyKey bodyKey); - - void applyRigidBodyImpulse(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z, - boolean hasOffset, - float offsetX, - float offsetY, - float offsetZ, - boolean torque); - - void applyRigidBodyForce(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z, - boolean hasOffset, - float offsetX, - float offsetY, - float offsetZ, - boolean torque); - - void createJoint(@Nonnull JointKey jointKey, - @Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB, - @Nonnull JointType type, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ, - float restLength, - float stiffness, - float damping, - float lowerLimit, - float upperLimit, - boolean motorEnabled, - float motorTargetVelocity, - float motorMaxForce); - - void destroyJoint(@Nonnull JointKey jointKey); - - void destroyJointBetween(@Nullable JointKey preferredJointKey, - @Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB); -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsCommandOperations.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsCommandOperations.java deleted file mode 100644 index 9e68e670..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsCommandOperations.java +++ /dev/null @@ -1,778 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.internal.simulation.batch.RigidBodySpawnBatch; -import dev.hytalemodding.impulse.core.internal.simulation.batch.RigidBodySpawnTemplateBatch; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.simulation.JointType; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandContext; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import java.util.Arrays; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Packed execution encoding for a frozen physics command context. - * - *

This is not the plugin authoring API. Plugins should compose commands through - * {@link PhysicsCommandContext} recipes and recorders.

- * - *

The representation keeps opcode, object, and float storage flat so bulk command recording - * does not allocate a wrapper object for every operation. Preserve that property on hot paths such - * as template spawns and batch ray setup.

- * - *

Each operation occupies one row in {@code opcodes}, {@code flags}, {@code objectOffsets}, - * and {@code floatOffsets}. The row stores only the operation kind, flag bits, and the starting - * offsets into two dense payload pools: {@code objects} for reference values and {@code floats} - * for scalar values. When a recorder appends an operation, {@link #add(byte, int, int, int)} - * reserves the opcode row plus the exact object/float slot counts for that opcode, then advances - * the payload sizes. A field read later resolves as {@code objects[objectOffsets[index] + slot]} - * or {@code floats[floatOffsets[index] + slot]}.

- * - *

The {@code *_SLOT} and {@code *_SLOTS} constants below define that per-opcode layout. - * Required object fields are decoded through {@link #requiredObjectAt(int, int, Class)}, while - * intentionally optional fields, such as the preferred joint key in - * {@link #DESTROY_JOINT_BETWEEN_BODIES}, stay nullable through {@link #objectAt(int, int)}.

- */ -public final class PhysicsCommandOperations { - - public static final byte SPAWN_RIGID_BODY = 1; - public static final byte SPAWN_RIGID_BODY_BATCH = 2; - public static final byte DESTROY_RIGID_BODY = 3; - public static final byte SET_RIGID_BODY_TRANSFORM = 4; - public static final byte SET_RIGID_BODY_VELOCITY = 5; - public static final byte SET_RIGID_BODY_TYPE = 6; - public static final byte ACTIVATE_RIGID_BODY = 7; - public static final byte APPLY_RIGID_BODY_IMPULSE = 8; - public static final byte APPLY_RIGID_BODY_FORCE = 9; - public static final byte CREATE_JOINT = 10; - public static final byte DESTROY_JOINT = 11; - public static final byte DESTROY_JOINT_BETWEEN_BODIES = 12; - public static final byte SPAWN_RIGID_BODY_TEMPLATE_BATCH = 14; - public static final byte SET_RIGID_BODY_POSITION = 15; - public static final byte SET_SPACE_GRAVITY = 16; - - public static final int FLAG_ACTIVATE = 1; - public static final int FLAG_OFFSET = 1 << 1; - public static final int FLAG_TORQUE = 1 << 2; - public static final int FLAG_MOTOR_ENABLED = 1 << 3; - - static final int SPAWN_OBJECT_SLOTS = 7; - static final int SPAWN_BODY_KEY_OBJECT_SLOT = 0; - static final int SPAWN_SPACE_ID_OBJECT_SLOT = 1; - static final int SPAWN_SHAPE_OBJECT_SLOT = 2; - static final int SPAWN_BODY_TYPE_OBJECT_SLOT = 3; - static final int SPAWN_SETTINGS_OBJECT_SLOT = 4; - static final int SPAWN_KIND_OBJECT_SLOT = 5; - static final int SPAWN_PERSISTENCE_MODE_OBJECT_SLOT = 6; - static final int SPAWN_FLOAT_SLOTS = 4; - static final int SPAWN_MASS_FLOAT_SLOT = 0; - static final int SPAWN_POSITION_X_FLOAT_SLOT = 1; - static final int SPAWN_POSITION_Y_FLOAT_SLOT = 2; - static final int SPAWN_POSITION_Z_FLOAT_SLOT = 3; - - static final int SPAWN_BATCH_OBJECT_SLOTS = 1; - static final int SPAWN_BATCH_OBJECT_SLOT = 0; - static final int SPAWN_TEMPLATE_BATCH_OBJECT_SLOTS = 1; - static final int SPAWN_TEMPLATE_BATCH_OBJECT_SLOT = 0; - - static final int BODY_COMMAND_OBJECT_SLOTS = 1; - static final int BODY_COMMAND_BODY_KEY_OBJECT_SLOT = 0; - - static final int SET_SPACE_GRAVITY_OBJECT_SLOTS = 1; - static final int SET_SPACE_GRAVITY_SPACE_ID_OBJECT_SLOT = 0; - static final int SET_SPACE_GRAVITY_FLOAT_SLOTS = 3; - static final int SET_SPACE_GRAVITY_X_FLOAT_SLOT = 0; - static final int SET_SPACE_GRAVITY_Y_FLOAT_SLOT = 1; - static final int SET_SPACE_GRAVITY_Z_FLOAT_SLOT = 2; - - static final int SET_TRANSFORM_FLOAT_SLOTS = 7; - static final int SET_TRANSFORM_POSITION_X_FLOAT_SLOT = 0; - static final int SET_TRANSFORM_POSITION_Y_FLOAT_SLOT = 1; - static final int SET_TRANSFORM_POSITION_Z_FLOAT_SLOT = 2; - static final int SET_TRANSFORM_ROTATION_X_FLOAT_SLOT = 3; - static final int SET_TRANSFORM_ROTATION_Y_FLOAT_SLOT = 4; - static final int SET_TRANSFORM_ROTATION_Z_FLOAT_SLOT = 5; - static final int SET_TRANSFORM_ROTATION_W_FLOAT_SLOT = 6; - - static final int SET_POSITION_FLOAT_SLOTS = 3; - static final int SET_POSITION_X_FLOAT_SLOT = 0; - static final int SET_POSITION_Y_FLOAT_SLOT = 1; - static final int SET_POSITION_Z_FLOAT_SLOT = 2; - - static final int SET_VELOCITY_FLOAT_SLOTS = 6; - static final int SET_VELOCITY_LINEAR_X_FLOAT_SLOT = 0; - static final int SET_VELOCITY_LINEAR_Y_FLOAT_SLOT = 1; - static final int SET_VELOCITY_LINEAR_Z_FLOAT_SLOT = 2; - static final int SET_VELOCITY_ANGULAR_X_FLOAT_SLOT = 3; - static final int SET_VELOCITY_ANGULAR_Y_FLOAT_SLOT = 4; - static final int SET_VELOCITY_ANGULAR_Z_FLOAT_SLOT = 5; - - static final int SET_TYPE_OBJECT_SLOTS = 2; - static final int SET_TYPE_BODY_KEY_OBJECT_SLOT = 0; - static final int SET_TYPE_BODY_TYPE_OBJECT_SLOT = 1; - - static final int VECTOR_COMMAND_FLOAT_SLOTS = 3; - static final int OFFSET_VECTOR_COMMAND_FLOAT_SLOTS = 6; - static final int VECTOR_COMMAND_X_FLOAT_SLOT = 0; - static final int VECTOR_COMMAND_Y_FLOAT_SLOT = 1; - static final int VECTOR_COMMAND_Z_FLOAT_SLOT = 2; - static final int VECTOR_COMMAND_OFFSET_X_FLOAT_SLOT = 3; - static final int VECTOR_COMMAND_OFFSET_Y_FLOAT_SLOT = 4; - static final int VECTOR_COMMAND_OFFSET_Z_FLOAT_SLOT = 5; - - static final int CREATE_JOINT_OBJECT_SLOTS = 5; - static final int CREATE_JOINT_JOINT_KEY_OBJECT_SLOT = 0; - static final int CREATE_JOINT_SPACE_ID_OBJECT_SLOT = 1; - static final int CREATE_JOINT_BODY_A_OBJECT_SLOT = 2; - static final int CREATE_JOINT_BODY_B_OBJECT_SLOT = 3; - static final int CREATE_JOINT_TYPE_OBJECT_SLOT = 4; - static final int CREATE_JOINT_FLOAT_SLOTS = 16; - static final int CREATE_JOINT_ANCHOR_A_X_FLOAT_SLOT = 0; - static final int CREATE_JOINT_ANCHOR_A_Y_FLOAT_SLOT = 1; - static final int CREATE_JOINT_ANCHOR_A_Z_FLOAT_SLOT = 2; - static final int CREATE_JOINT_ANCHOR_B_X_FLOAT_SLOT = 3; - static final int CREATE_JOINT_ANCHOR_B_Y_FLOAT_SLOT = 4; - static final int CREATE_JOINT_ANCHOR_B_Z_FLOAT_SLOT = 5; - static final int CREATE_JOINT_AXIS_X_FLOAT_SLOT = 6; - static final int CREATE_JOINT_AXIS_Y_FLOAT_SLOT = 7; - static final int CREATE_JOINT_AXIS_Z_FLOAT_SLOT = 8; - static final int CREATE_JOINT_REST_LENGTH_FLOAT_SLOT = 9; - static final int CREATE_JOINT_STIFFNESS_FLOAT_SLOT = 10; - static final int CREATE_JOINT_DAMPING_FLOAT_SLOT = 11; - static final int CREATE_JOINT_LOWER_LIMIT_FLOAT_SLOT = 12; - static final int CREATE_JOINT_UPPER_LIMIT_FLOAT_SLOT = 13; - static final int CREATE_JOINT_MOTOR_TARGET_VELOCITY_FLOAT_SLOT = 14; - static final int CREATE_JOINT_MOTOR_MAX_FORCE_FLOAT_SLOT = 15; - - static final int DESTROY_JOINT_OBJECT_SLOTS = 1; - static final int DESTROY_JOINT_KEY_OBJECT_SLOT = 0; - static final int DESTROY_JOINT_BETWEEN_OBJECT_SLOTS = 4; - static final int DESTROY_JOINT_BETWEEN_PREFERRED_KEY_OBJECT_SLOT = 0; - static final int DESTROY_JOINT_BETWEEN_SPACE_ID_OBJECT_SLOT = 1; - static final int DESTROY_JOINT_BETWEEN_BODY_A_OBJECT_SLOT = 2; - static final int DESTROY_JOINT_BETWEEN_BODY_B_OBJECT_SLOT = 3; - - private byte[] opcodes; - private int[] objectOffsets; - private int[] floatOffsets; - private Object[] objects; - private float[] floats; - private int[] flags; - private int size; - private int objectSize; - private int floatSize; - - public PhysicsCommandOperations(int expectedOps) { - int capacity = Math.max(1, expectedOps); - opcodes = new byte[capacity]; - objectOffsets = new int[capacity]; - floatOffsets = new int[capacity]; - objects = new Object[capacity * 2]; - floats = new float[capacity * 4]; - flags = new int[capacity]; - } - - private PhysicsCommandOperations(@Nonnull byte[] opcodes, - @Nonnull int[] objectOffsets, - @Nonnull int[] floatOffsets, - @Nonnull Object[] objects, - @Nonnull float[] floats, - @Nonnull int[] flags, - int size, - int objectSize, - int floatSize) { - this.opcodes = opcodes; - this.objectOffsets = objectOffsets; - this.floatOffsets = floatOffsets; - this.objects = objects; - this.floats = floats; - this.flags = flags; - this.size = size; - this.objectSize = objectSize; - this.floatSize = floatSize; - } - - @Nonnull - public PhysicsCommandOperations freeze() { - return new PhysicsCommandOperations( - freeze(opcodes, size), - freeze(objectOffsets, size), - freeze(floatOffsets, size), - freeze(objects, objectSize), - freeze(floats, floatSize), - freeze(flags, size), - size, - objectSize, - floatSize); - } - - public void addSpawn(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - float positionX, - float positionY, - float positionZ, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - int index = add(SPAWN_RIGID_BODY, 0, SPAWN_OBJECT_SLOTS, SPAWN_FLOAT_SLOTS); - object(index, SPAWN_BODY_KEY_OBJECT_SLOT, bodyKey); - object(index, SPAWN_SPACE_ID_OBJECT_SLOT, spaceId); - object(index, SPAWN_SHAPE_OBJECT_SLOT, shape); - object(index, SPAWN_BODY_TYPE_OBJECT_SLOT, bodyType); - object(index, SPAWN_SETTINGS_OBJECT_SLOT, settings); - object(index, SPAWN_KIND_OBJECT_SLOT, kind); - object(index, SPAWN_PERSISTENCE_MODE_OBJECT_SLOT, persistenceMode); - floatAt(index, SPAWN_MASS_FLOAT_SLOT, mass); - floatAt(index, SPAWN_POSITION_X_FLOAT_SLOT, positionX); - floatAt(index, SPAWN_POSITION_Y_FLOAT_SLOT, positionY); - floatAt(index, SPAWN_POSITION_Z_FLOAT_SLOT, positionZ); - } - - public void addSpawnBatch(@Nonnull RigidBodySpawnBatch batch) { - if (batch.size() <= 0) { - return; - } - int index = add(SPAWN_RIGID_BODY_BATCH, 0, SPAWN_BATCH_OBJECT_SLOTS, 0); - object(index, SPAWN_BATCH_OBJECT_SLOT, batch.freeze()); - } - - public void addSpawnTemplateBatch(@Nonnull RigidBodySpawnTemplateBatch batch) { - if (batch.size() <= 0) { - return; - } - int index = add(SPAWN_RIGID_BODY_TEMPLATE_BATCH, 0, SPAWN_TEMPLATE_BATCH_OBJECT_SLOTS, 0); - object(index, SPAWN_TEMPLATE_BATCH_OBJECT_SLOT, batch.freeze()); - } - - public void addDestroyBody(@Nonnull RigidBodyKey bodyKey) { - int index = add(DESTROY_RIGID_BODY, 0, BODY_COMMAND_OBJECT_SLOTS, 0); - object(index, BODY_COMMAND_BODY_KEY_OBJECT_SLOT, bodyKey); - } - - public void addSetSpaceGravity(@Nonnull SpaceId spaceId, - float x, - float y, - float z) { - int index = add(SET_SPACE_GRAVITY, 0, SET_SPACE_GRAVITY_OBJECT_SLOTS, SET_SPACE_GRAVITY_FLOAT_SLOTS); - object(index, SET_SPACE_GRAVITY_SPACE_ID_OBJECT_SLOT, spaceId); - floatAt(index, SET_SPACE_GRAVITY_X_FLOAT_SLOT, x); - floatAt(index, SET_SPACE_GRAVITY_Y_FLOAT_SLOT, y); - floatAt(index, SET_SPACE_GRAVITY_Z_FLOAT_SLOT, z); - } - - public void addSetTransform(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ, - float rotationX, - float rotationY, - float rotationZ, - float rotationW, - boolean activate) { - int index = add(SET_RIGID_BODY_TRANSFORM, - activate ? FLAG_ACTIVATE : 0, - BODY_COMMAND_OBJECT_SLOTS, - SET_TRANSFORM_FLOAT_SLOTS); - object(index, BODY_COMMAND_BODY_KEY_OBJECT_SLOT, bodyKey); - floatAt(index, SET_TRANSFORM_POSITION_X_FLOAT_SLOT, positionX); - floatAt(index, SET_TRANSFORM_POSITION_Y_FLOAT_SLOT, positionY); - floatAt(index, SET_TRANSFORM_POSITION_Z_FLOAT_SLOT, positionZ); - floatAt(index, SET_TRANSFORM_ROTATION_X_FLOAT_SLOT, rotationX); - floatAt(index, SET_TRANSFORM_ROTATION_Y_FLOAT_SLOT, rotationY); - floatAt(index, SET_TRANSFORM_ROTATION_Z_FLOAT_SLOT, rotationZ); - floatAt(index, SET_TRANSFORM_ROTATION_W_FLOAT_SLOT, rotationW); - } - - public void addSetPosition(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ, - boolean activate) { - int index = add(SET_RIGID_BODY_POSITION, - activate ? FLAG_ACTIVATE : 0, - BODY_COMMAND_OBJECT_SLOTS, - SET_POSITION_FLOAT_SLOTS); - object(index, BODY_COMMAND_BODY_KEY_OBJECT_SLOT, bodyKey); - floatAt(index, SET_POSITION_X_FLOAT_SLOT, positionX); - floatAt(index, SET_POSITION_Y_FLOAT_SLOT, positionY); - floatAt(index, SET_POSITION_Z_FLOAT_SLOT, positionZ); - } - - public void addSetVelocity(@Nonnull RigidBodyKey bodyKey, - float linearX, - float linearY, - float linearZ, - float angularX, - float angularY, - float angularZ, - boolean activate) { - int index = add(SET_RIGID_BODY_VELOCITY, - activate ? FLAG_ACTIVATE : 0, - BODY_COMMAND_OBJECT_SLOTS, - SET_VELOCITY_FLOAT_SLOTS); - object(index, BODY_COMMAND_BODY_KEY_OBJECT_SLOT, bodyKey); - floatAt(index, SET_VELOCITY_LINEAR_X_FLOAT_SLOT, linearX); - floatAt(index, SET_VELOCITY_LINEAR_Y_FLOAT_SLOT, linearY); - floatAt(index, SET_VELOCITY_LINEAR_Z_FLOAT_SLOT, linearZ); - floatAt(index, SET_VELOCITY_ANGULAR_X_FLOAT_SLOT, angularX); - floatAt(index, SET_VELOCITY_ANGULAR_Y_FLOAT_SLOT, angularY); - floatAt(index, SET_VELOCITY_ANGULAR_Z_FLOAT_SLOT, angularZ); - } - - public void addSetType(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodyType bodyType, - boolean activate) { - int index = add(SET_RIGID_BODY_TYPE, activate ? FLAG_ACTIVATE : 0, SET_TYPE_OBJECT_SLOTS, 0); - object(index, SET_TYPE_BODY_KEY_OBJECT_SLOT, bodyKey); - object(index, SET_TYPE_BODY_TYPE_OBJECT_SLOT, bodyType); - } - - public void addActivate(@Nonnull RigidBodyKey bodyKey) { - int index = add(ACTIVATE_RIGID_BODY, 0, BODY_COMMAND_OBJECT_SLOTS, 0); - object(index, BODY_COMMAND_BODY_KEY_OBJECT_SLOT, bodyKey); - } - - public void addImpulse(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z, - boolean hasOffset, - float offsetX, - float offsetY, - float offsetZ, - boolean torque) { - int operationFlags = torque ? FLAG_TORQUE : 0; - if (hasOffset) { - operationFlags |= FLAG_OFFSET; - } - int index = add(APPLY_RIGID_BODY_IMPULSE, - operationFlags, - BODY_COMMAND_OBJECT_SLOTS, - (operationFlags & FLAG_OFFSET) != 0 ? OFFSET_VECTOR_COMMAND_FLOAT_SLOTS : VECTOR_COMMAND_FLOAT_SLOTS); - object(index, BODY_COMMAND_BODY_KEY_OBJECT_SLOT, bodyKey); - floatAt(index, VECTOR_COMMAND_X_FLOAT_SLOT, x); - floatAt(index, VECTOR_COMMAND_Y_FLOAT_SLOT, y); - floatAt(index, VECTOR_COMMAND_Z_FLOAT_SLOT, z); - if ((operationFlags & FLAG_OFFSET) != 0) { - floatAt(index, VECTOR_COMMAND_OFFSET_X_FLOAT_SLOT, offsetX); - floatAt(index, VECTOR_COMMAND_OFFSET_Y_FLOAT_SLOT, offsetY); - floatAt(index, VECTOR_COMMAND_OFFSET_Z_FLOAT_SLOT, offsetZ); - } - } - - public void addForce(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z, - boolean hasOffset, - float offsetX, - float offsetY, - float offsetZ, - boolean torque) { - int operationFlags = torque ? FLAG_TORQUE : 0; - if (hasOffset) { - operationFlags |= FLAG_OFFSET; - } - int index = add(APPLY_RIGID_BODY_FORCE, - operationFlags, - BODY_COMMAND_OBJECT_SLOTS, - (operationFlags & FLAG_OFFSET) != 0 ? OFFSET_VECTOR_COMMAND_FLOAT_SLOTS : VECTOR_COMMAND_FLOAT_SLOTS); - object(index, BODY_COMMAND_BODY_KEY_OBJECT_SLOT, bodyKey); - floatAt(index, VECTOR_COMMAND_X_FLOAT_SLOT, x); - floatAt(index, VECTOR_COMMAND_Y_FLOAT_SLOT, y); - floatAt(index, VECTOR_COMMAND_Z_FLOAT_SLOT, z); - if ((operationFlags & FLAG_OFFSET) != 0) { - floatAt(index, VECTOR_COMMAND_OFFSET_X_FLOAT_SLOT, offsetX); - floatAt(index, VECTOR_COMMAND_OFFSET_Y_FLOAT_SLOT, offsetY); - floatAt(index, VECTOR_COMMAND_OFFSET_Z_FLOAT_SLOT, offsetZ); - } - } - - public void addJoint(@Nonnull JointKey jointKey, - @Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB, - @Nonnull JointType type, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ, - float restLength, - float stiffness, - float damping, - float lowerLimit, - float upperLimit, - boolean motorEnabled, - float motorTargetVelocity, - float motorMaxForce) { - int index = add(CREATE_JOINT, - motorEnabled ? FLAG_MOTOR_ENABLED : 0, - CREATE_JOINT_OBJECT_SLOTS, - CREATE_JOINT_FLOAT_SLOTS); - object(index, CREATE_JOINT_JOINT_KEY_OBJECT_SLOT, jointKey); - object(index, CREATE_JOINT_SPACE_ID_OBJECT_SLOT, spaceId); - object(index, CREATE_JOINT_BODY_A_OBJECT_SLOT, bodyA); - object(index, CREATE_JOINT_BODY_B_OBJECT_SLOT, bodyB); - object(index, CREATE_JOINT_TYPE_OBJECT_SLOT, type); - floatAt(index, CREATE_JOINT_ANCHOR_A_X_FLOAT_SLOT, anchorAX); - floatAt(index, CREATE_JOINT_ANCHOR_A_Y_FLOAT_SLOT, anchorAY); - floatAt(index, CREATE_JOINT_ANCHOR_A_Z_FLOAT_SLOT, anchorAZ); - floatAt(index, CREATE_JOINT_ANCHOR_B_X_FLOAT_SLOT, anchorBX); - floatAt(index, CREATE_JOINT_ANCHOR_B_Y_FLOAT_SLOT, anchorBY); - floatAt(index, CREATE_JOINT_ANCHOR_B_Z_FLOAT_SLOT, anchorBZ); - floatAt(index, CREATE_JOINT_AXIS_X_FLOAT_SLOT, axisX); - floatAt(index, CREATE_JOINT_AXIS_Y_FLOAT_SLOT, axisY); - floatAt(index, CREATE_JOINT_AXIS_Z_FLOAT_SLOT, axisZ); - floatAt(index, CREATE_JOINT_REST_LENGTH_FLOAT_SLOT, restLength); - floatAt(index, CREATE_JOINT_STIFFNESS_FLOAT_SLOT, stiffness); - floatAt(index, CREATE_JOINT_DAMPING_FLOAT_SLOT, damping); - floatAt(index, CREATE_JOINT_LOWER_LIMIT_FLOAT_SLOT, lowerLimit); - floatAt(index, CREATE_JOINT_UPPER_LIMIT_FLOAT_SLOT, upperLimit); - floatAt(index, CREATE_JOINT_MOTOR_TARGET_VELOCITY_FLOAT_SLOT, motorTargetVelocity); - floatAt(index, CREATE_JOINT_MOTOR_MAX_FORCE_FLOAT_SLOT, motorMaxForce); - } - - public void addDestroyJoint(@Nonnull JointKey jointKey) { - int index = add(DESTROY_JOINT, 0, DESTROY_JOINT_OBJECT_SLOTS, 0); - object(index, DESTROY_JOINT_KEY_OBJECT_SLOT, jointKey); - } - - public void addDestroyJointBetween(@Nullable JointKey preferredJointKey, - @Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB) { - int index = add(DESTROY_JOINT_BETWEEN_BODIES, 0, DESTROY_JOINT_BETWEEN_OBJECT_SLOTS, 0); - object(index, DESTROY_JOINT_BETWEEN_PREFERRED_KEY_OBJECT_SLOT, preferredJointKey); - object(index, DESTROY_JOINT_BETWEEN_SPACE_ID_OBJECT_SLOT, spaceId); - object(index, DESTROY_JOINT_BETWEEN_BODY_A_OBJECT_SLOT, bodyA); - object(index, DESTROY_JOINT_BETWEEN_BODY_B_OBJECT_SLOT, bodyB); - } - - public int size() { - return size; - } - - @Nonnull - public RecordedBodyCreationKeys bodyCreationKeys() { - RecordedBodyCreationKeys.Builder keys = RecordedBodyCreationKeys.builder(); - for (int index = 0; index < size; index++) { - switch (opcode(index)) { - case SPAWN_RIGID_BODY -> keys.add( - requiredObjectAt(index, SPAWN_BODY_KEY_OBJECT_SLOT, RigidBodyKey.class)); - case SPAWN_RIGID_BODY_BATCH -> { - RigidBodySpawnBatch batch = - requiredObjectAt(index, SPAWN_BATCH_OBJECT_SLOT, RigidBodySpawnBatch.class); - keys.addAll(batch.bodyKeyMostSignificantBits(), - batch.bodyKeyLeastSignificantBits(), - batch.size()); - } - case SPAWN_RIGID_BODY_TEMPLATE_BATCH -> { - RigidBodySpawnTemplateBatch batch = - requiredObjectAt(index, - SPAWN_TEMPLATE_BATCH_OBJECT_SLOT, - RigidBodySpawnTemplateBatch.class); - keys.addAll(batch.bodyKeyMostSignificantBits(), - batch.bodyKeyLeastSignificantBits(), - batch.size()); - } - default -> { - } - } - } - return keys.build(); - } - - @Nonnull - public EntityReferences entityReferences() { - EntityReferenceAccumulator accumulator = new EntityReferenceAccumulator(); - for (int index = 0; index < size; index++) { - switch (opcode(index)) { - case SPAWN_RIGID_BODY, - DESTROY_RIGID_BODY, - SET_RIGID_BODY_TRANSFORM, - SET_RIGID_BODY_POSITION, - SET_RIGID_BODY_VELOCITY, - SET_RIGID_BODY_TYPE, - ACTIVATE_RIGID_BODY, - APPLY_RIGID_BODY_IMPULSE, - APPLY_RIGID_BODY_FORCE -> accumulator.addBody( - requiredObjectAt(index, BODY_COMMAND_BODY_KEY_OBJECT_SLOT, RigidBodyKey.class)); - case SPAWN_RIGID_BODY_BATCH -> - accumulator.addSpawnBatch( - requiredObjectAt(index, SPAWN_BATCH_OBJECT_SLOT, RigidBodySpawnBatch.class)); - case SPAWN_RIGID_BODY_TEMPLATE_BATCH -> - accumulator.addSpawnTemplateBatch( - requiredObjectAt(index, - SPAWN_TEMPLATE_BATCH_OBJECT_SLOT, - RigidBodySpawnTemplateBatch.class)); - case CREATE_JOINT -> { - accumulator.addJoint(requiredObjectAt(index, - CREATE_JOINT_JOINT_KEY_OBJECT_SLOT, - JointKey.class)); - accumulator.addBody(requiredObjectAt(index, - CREATE_JOINT_BODY_A_OBJECT_SLOT, - RigidBodyKey.class)); - accumulator.addBody(requiredObjectAt(index, - CREATE_JOINT_BODY_B_OBJECT_SLOT, - RigidBodyKey.class)); - } - case DESTROY_JOINT -> accumulator.addJoint( - requiredObjectAt(index, DESTROY_JOINT_KEY_OBJECT_SLOT, JointKey.class)); - case DESTROY_JOINT_BETWEEN_BODIES -> { - JointKey jointKey = (JointKey) objectAt(index, - DESTROY_JOINT_BETWEEN_PREFERRED_KEY_OBJECT_SLOT); - if (jointKey != null) { - accumulator.addJoint(jointKey); - } - accumulator.addBody(requiredObjectAt(index, - DESTROY_JOINT_BETWEEN_BODY_A_OBJECT_SLOT, - RigidBodyKey.class)); - accumulator.addBody(requiredObjectAt(index, - DESTROY_JOINT_BETWEEN_BODY_B_OBJECT_SLOT, - RigidBodyKey.class)); - } - default -> { - } - } - } - return accumulator.references(); - } - - public byte opcode(int index) { - checkIndex(index); - return opcodes[index]; - } - - public int flags(int index) { - checkIndex(index); - return flags[index]; - } - - public float floatAt(int index, - int slot) { - checkIndex(index); - return floats[floatOffsets[index] + slot]; - } - - @Nullable - public Object objectAt(int index, - int slot) { - checkIndex(index); - return objects[objectOffsets[index] + slot]; - } - - @Nullable - public T objectAt(int index, - int slot, - @Nonnull Class type) { - return type.cast(objectAt(index, slot)); - } - - @Nonnull - public T requiredObjectAt(int index, - int slot, - @Nonnull Class type) { - T value = objectAt(index, slot, type); - if (value == null) { - throw new IllegalArgumentException("Missing physics command object at index=" - + index - + " slot=" - + slot - + " type=" - + type.getSimpleName()); - } - return value; - } - - private int add(byte opcode, - int operationFlags, - int objectSlots, - int floatSlots) { - ensureOperationCapacity(size + 1); - ensureObjectCapacity(objectSize + objectSlots); - ensureFloatCapacity(floatSize + floatSlots); - int index = size++; - opcodes[index] = opcode; - flags[index] = operationFlags; - objectOffsets[index] = objectSize; - floatOffsets[index] = floatSize; - objectSize += objectSlots; - floatSize += floatSlots; - return index; - } - - private void object(int index, - int slot, - @Nullable Object value) { - objects[objectOffsets[index] + slot] = value; - } - - private void floatAt(int index, - int slot, - float value) { - floats[floatOffsets[index] + slot] = value; - } - - private void checkIndex(int index) { - if (index < 0 || index >= size) { - throw new IndexOutOfBoundsException(index); - } - } - - private void ensureOperationCapacity(int required) { - if (required <= opcodes.length) { - return; - } - int nextCapacity = Math.max(required, opcodes.length + (opcodes.length >> 1)); - opcodes = Arrays.copyOf(opcodes, nextCapacity); - objectOffsets = Arrays.copyOf(objectOffsets, nextCapacity); - floatOffsets = Arrays.copyOf(floatOffsets, nextCapacity); - flags = Arrays.copyOf(flags, nextCapacity); - } - - private void ensureObjectCapacity(int required) { - if (required <= objects.length) { - return; - } - int nextCapacity = Math.max(required, objects.length + (objects.length >> 1)); - objects = Arrays.copyOf(objects, nextCapacity); - } - - private void ensureFloatCapacity(int required) { - if (required <= floats.length) { - return; - } - int nextCapacity = Math.max(required, floats.length + (floats.length >> 1)); - floats = Arrays.copyOf(floats, nextCapacity); - } - - @Nonnull - private static byte[] freeze(@Nonnull byte[] values, - int size) { - return values.length == size ? values : Arrays.copyOf(values, size); - } - - @Nonnull - private static int[] freeze(@Nonnull int[] values, - int size) { - return values.length == size ? values : Arrays.copyOf(values, size); - } - - @Nonnull - private static Object[] freeze(@Nonnull Object[] values, - int size) { - return values.length == size ? values : Arrays.copyOf(values, size); - } - - @Nonnull - private static float[] freeze(@Nonnull float[] values, - int size) { - return values.length == size ? values : Arrays.copyOf(values, size); - } - - public static final class EntityReferences { - - private final int bodyKeyReferenceCount; - @Nullable - private final RigidBodyKey firstBodyKey; - private final int jointKeyReferenceCount; - @Nullable - private final JointKey firstJointKey; - - private EntityReferences(int bodyKeyReferenceCount, - @Nullable RigidBodyKey firstBodyKey, - int jointKeyReferenceCount, - @Nullable JointKey firstJointKey) { - this.bodyKeyReferenceCount = Math.max(0, bodyKeyReferenceCount); - this.firstBodyKey = this.bodyKeyReferenceCount > 0 ? firstBodyKey : null; - this.jointKeyReferenceCount = Math.max(0, jointKeyReferenceCount); - this.firstJointKey = this.jointKeyReferenceCount > 0 ? firstJointKey : null; - } - - public int bodyKeyReferenceCount() { - return bodyKeyReferenceCount; - } - - @Nullable - public RigidBodyKey firstBodyKey() { - return firstBodyKey; - } - - public int jointKeyReferenceCount() { - return jointKeyReferenceCount; - } - - @Nullable - public JointKey firstJointKey() { - return firstJointKey; - } - } - - private static final class EntityReferenceAccumulator { - - private int bodyKeyReferenceCount; - @Nullable - private RigidBodyKey firstBodyKey; - private int jointKeyReferenceCount; - @Nullable - private JointKey firstJointKey; - - void addBody(@Nonnull RigidBodyKey bodyKey) { - if (firstBodyKey == null) { - firstBodyKey = bodyKey; - } - bodyKeyReferenceCount++; - } - - void addSpawnBatch(@Nonnull RigidBodySpawnBatch batch) { - int batchSize = batch.size(); - if (batchSize <= 0) { - return; - } - if (firstBodyKey == null) { - firstBodyKey = batch.bodyKey(0); - } - bodyKeyReferenceCount += batchSize; - } - - void addSpawnTemplateBatch(@Nonnull RigidBodySpawnTemplateBatch batch) { - int batchSize = batch.size(); - if (batchSize <= 0) { - return; - } - if (firstBodyKey == null) { - firstBodyKey = batch.bodyKey(0); - } - bodyKeyReferenceCount += batchSize; - } - - void addJoint(@Nonnull JointKey jointKey) { - if (firstJointKey == null) { - firstJointKey = jointKey; - } - jointKeyReferenceCount++; - } - - @Nonnull - EntityReferences references() { - return new EntityReferences(bodyKeyReferenceCount, - firstBodyKey, - jointKeyReferenceCount, - firstJointKey); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsSimulationExecutor.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsSimulationExecutor.java deleted file mode 100644 index 8debc6a7..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsSimulationExecutor.java +++ /dev/null @@ -1,1711 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation; - -import com.hypixel.hytale.math.util.ChunkUtil; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.runtime.BackendRayHitSink; -import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; -import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; -import dev.hytalemodding.impulse.core.internal.resources.BackendJointHandle; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistration; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodySnapshots; -import dev.hytalemodding.impulse.core.internal.resources.joint.PhysicsJointRegistration; -import dev.hytalemodding.impulse.core.internal.simulation.batch.RecordedPhysicsCommandBatch; -import dev.hytalemodding.impulse.core.internal.simulation.batch.RigidBodySpawnBatch; -import dev.hytalemodding.impulse.core.internal.simulation.batch.RigidBodySpawnTemplateBatch; -import dev.hytalemodding.impulse.core.internal.simulation.query.BenchmarkSpaceStatsQuery; -import dev.hytalemodding.impulse.core.internal.simulation.query.PhysicsDebugContactsQuery; -import dev.hytalemodding.impulse.core.internal.simulation.query.PhysicsDebugJointsQuery; -import dev.hytalemodding.impulse.core.internal.simulation.query.PhysicsInternalQuery; -import dev.hytalemodding.impulse.core.internal.simulation.query.PhysicsSpaceRuntimeStatsQuery; -import dev.hytalemodding.impulse.core.internal.simulation.query.WorldCollisionPrewarmEnvelopeQuery; -import dev.hytalemodding.impulse.core.internal.simulation.view.BenchmarkSpaceStatsView; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsDebugContactView; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsDebugJointView; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsSpaceRuntimeStatsView; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldCollisionBuildOptions; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldVoxelCollisionCache; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldVoxelCollisionCache.BuildStats; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionBuildStats; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionPrewarmStats; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.simulation.query.CcdSupportQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.JointType; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandCompletion; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandResult; -import dev.hytalemodding.impulse.core.plugin.simulation.query.PhysicsQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RaycastAllQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RaycastClosestBatchQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.RaycastClosestBatchResult; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RaycastClosestQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.view.RaycastHitView; -import dev.hytalemodding.impulse.core.plugin.simulation.RaycastSegment; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RuntimeJointCountQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodyPose; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RigidBodyStateQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.view.RigidBodyStateView; -import dev.hytalemodding.impulse.core.plugin.simulation.query.SolverCapabilityQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.SolverCapabilitySummary; -import dev.hytalemodding.impulse.core.plugin.simulation.query.SpaceBodyCountQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.SpaceSummary; -import dev.hytalemodding.impulse.core.plugin.simulation.query.SpaceSummaryQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.query.UnsupportedCcdSpacesQuery; -import it.unimi.dsi.fastutil.longs.LongOpenHashSet; -import it.unimi.dsi.fastutil.longs.LongSet; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3d; -import org.joml.Vector3f; - -/** - * Owner-lane translator from public simulation commands to live backend calls. - */ -public final class PhysicsSimulationExecutor implements PhysicsCommandDispatcher { - - private static final float CONTACT_NORMAL_SCALE = 0.75f; - private static final float JOINT_AXIS_SCALE = 0.9f; - - @Nonnull - private final PhysicsWorldRuntimeResource runtime; - - public PhysicsSimulationExecutor(@Nonnull PhysicsWorldRuntimeResource runtime) { - this.runtime = Objects.requireNonNull(runtime, "runtime"); - } - - @Nonnull - public PhysicsCommandCompletion execute(@Nonnull RecordedPhysicsCommandBatch batch) { - Objects.requireNonNull(batch, "batch"); - long currentWorldEpoch = runtime.commandWorldEpoch(); - if (batch.commandWorldEpoch() != currentWorldEpoch) { - return rejectStaleBatch(batch, currentWorldEpoch); - } - PhysicsCommandOperations operations = batch.operations(); - List results = null; - for (int index = 0; index < operations.size(); index++) { - long commandSequence = index + 1L; - try { - dispatch(index, operations); - if (results != null) { - results.add(PhysicsCommandResult.applied(batch.metadata(), commandSequence)); - } - } catch (RuntimeException exception) { - if (results == null) { - results = new ArrayList<>(operations.size()); - for (int appliedIndex = 0; appliedIndex < index; appliedIndex++) { - results.add(PhysicsCommandResult.applied(batch.metadata(), appliedIndex + 1L)); - } - } - results.add(PhysicsCommandResult.rejected(batch.metadata(), - commandSequence, - exception.getMessage() != null ? exception.getMessage() : exception.getClass().getSimpleName())); - } - } - return results != null - ? PhysicsCommandCompletion.of(results) - : PhysicsCommandCompletion.allApplied(batch.metadata(), operations.size()); - } - - @Nonnull - private static PhysicsCommandCompletion rejectStaleBatch(@Nonnull RecordedPhysicsCommandBatch batch, - long currentWorldEpoch) { - String message = "stale physics command batch worldEpoch=" - + batch.commandWorldEpoch() - + " currentWorldEpoch=" - + currentWorldEpoch; - return PhysicsCommandCompletion.allRejected(batch.metadata(), batch.commandCount(), message); - } - - private void dispatch(int index, - @Nonnull PhysicsCommandOperations operations) { - switch (operations.opcode(index)) { - case PhysicsCommandOperations.SPAWN_RIGID_BODY -> dispatchSpawn(index, operations); - case PhysicsCommandOperations.SPAWN_RIGID_BODY_BATCH -> - dispatchSpawnBatch(operations.requiredObjectAt(index, - PhysicsCommandOperations.SPAWN_BATCH_OBJECT_SLOT, - RigidBodySpawnBatch.class)); - case PhysicsCommandOperations.SPAWN_RIGID_BODY_TEMPLATE_BATCH -> - dispatchSpawnTemplateBatch(operations.requiredObjectAt(index, - PhysicsCommandOperations.SPAWN_TEMPLATE_BATCH_OBJECT_SLOT, - RigidBodySpawnTemplateBatch.class)); - case PhysicsCommandOperations.DESTROY_RIGID_BODY -> destroyRigidBody( - operations.requiredObjectAt(index, - PhysicsCommandOperations.BODY_COMMAND_BODY_KEY_OBJECT_SLOT, - RigidBodyKey.class)); - case PhysicsCommandOperations.SET_SPACE_GRAVITY -> setSpaceGravity( - operations.requiredObjectAt(index, - PhysicsCommandOperations.SET_SPACE_GRAVITY_SPACE_ID_OBJECT_SLOT, - SpaceId.class), - operations.floatAt(index, PhysicsCommandOperations.SET_SPACE_GRAVITY_X_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SET_SPACE_GRAVITY_Y_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SET_SPACE_GRAVITY_Z_FLOAT_SLOT)); - case PhysicsCommandOperations.SET_RIGID_BODY_TRANSFORM -> setRigidBodyTransform( - operations.requiredObjectAt(index, - PhysicsCommandOperations.BODY_COMMAND_BODY_KEY_OBJECT_SLOT, - RigidBodyKey.class), - operations.floatAt(index, PhysicsCommandOperations.SET_TRANSFORM_POSITION_X_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SET_TRANSFORM_POSITION_Y_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SET_TRANSFORM_POSITION_Z_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SET_TRANSFORM_ROTATION_X_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SET_TRANSFORM_ROTATION_Y_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SET_TRANSFORM_ROTATION_Z_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SET_TRANSFORM_ROTATION_W_FLOAT_SLOT), - (operations.flags(index) & PhysicsCommandOperations.FLAG_ACTIVATE) != 0); - case PhysicsCommandOperations.SET_RIGID_BODY_POSITION -> setRigidBodyPosition( - operations.requiredObjectAt(index, - PhysicsCommandOperations.BODY_COMMAND_BODY_KEY_OBJECT_SLOT, - RigidBodyKey.class), - operations.floatAt(index, PhysicsCommandOperations.SET_POSITION_X_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SET_POSITION_Y_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SET_POSITION_Z_FLOAT_SLOT), - (operations.flags(index) & PhysicsCommandOperations.FLAG_ACTIVATE) != 0); - case PhysicsCommandOperations.SET_RIGID_BODY_VELOCITY -> setRigidBodyVelocity( - operations.requiredObjectAt(index, - PhysicsCommandOperations.BODY_COMMAND_BODY_KEY_OBJECT_SLOT, - RigidBodyKey.class), - operations.floatAt(index, PhysicsCommandOperations.SET_VELOCITY_LINEAR_X_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SET_VELOCITY_LINEAR_Y_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SET_VELOCITY_LINEAR_Z_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SET_VELOCITY_ANGULAR_X_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SET_VELOCITY_ANGULAR_Y_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SET_VELOCITY_ANGULAR_Z_FLOAT_SLOT), - (operations.flags(index) & PhysicsCommandOperations.FLAG_ACTIVATE) != 0); - case PhysicsCommandOperations.SET_RIGID_BODY_TYPE -> setRigidBodyType( - operations.requiredObjectAt(index, - PhysicsCommandOperations.SET_TYPE_BODY_KEY_OBJECT_SLOT, - RigidBodyKey.class), - operations.requiredObjectAt(index, - PhysicsCommandOperations.SET_TYPE_BODY_TYPE_OBJECT_SLOT, - PhysicsBodyType.class), - (operations.flags(index) & PhysicsCommandOperations.FLAG_ACTIVATE) != 0); - case PhysicsCommandOperations.ACTIVATE_RIGID_BODY -> activateRigidBody( - operations.requiredObjectAt(index, - PhysicsCommandOperations.BODY_COMMAND_BODY_KEY_OBJECT_SLOT, - RigidBodyKey.class)); - case PhysicsCommandOperations.APPLY_RIGID_BODY_IMPULSE -> applyRigidBodyImpulse( - operations.requiredObjectAt(index, - PhysicsCommandOperations.BODY_COMMAND_BODY_KEY_OBJECT_SLOT, - RigidBodyKey.class), - operations.floatAt(index, PhysicsCommandOperations.VECTOR_COMMAND_X_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.VECTOR_COMMAND_Y_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.VECTOR_COMMAND_Z_FLOAT_SLOT), - (operations.flags(index) & PhysicsCommandOperations.FLAG_OFFSET) != 0, - (operations.flags(index) & PhysicsCommandOperations.FLAG_OFFSET) != 0 - ? operations.floatAt(index, PhysicsCommandOperations.VECTOR_COMMAND_OFFSET_X_FLOAT_SLOT) - : 0.0f, - (operations.flags(index) & PhysicsCommandOperations.FLAG_OFFSET) != 0 - ? operations.floatAt(index, PhysicsCommandOperations.VECTOR_COMMAND_OFFSET_Y_FLOAT_SLOT) - : 0.0f, - (operations.flags(index) & PhysicsCommandOperations.FLAG_OFFSET) != 0 - ? operations.floatAt(index, PhysicsCommandOperations.VECTOR_COMMAND_OFFSET_Z_FLOAT_SLOT) - : 0.0f, - (operations.flags(index) & PhysicsCommandOperations.FLAG_TORQUE) != 0); - case PhysicsCommandOperations.APPLY_RIGID_BODY_FORCE -> applyRigidBodyForce( - operations.requiredObjectAt(index, - PhysicsCommandOperations.BODY_COMMAND_BODY_KEY_OBJECT_SLOT, - RigidBodyKey.class), - operations.floatAt(index, PhysicsCommandOperations.VECTOR_COMMAND_X_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.VECTOR_COMMAND_Y_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.VECTOR_COMMAND_Z_FLOAT_SLOT), - (operations.flags(index) & PhysicsCommandOperations.FLAG_OFFSET) != 0, - (operations.flags(index) & PhysicsCommandOperations.FLAG_OFFSET) != 0 - ? operations.floatAt(index, PhysicsCommandOperations.VECTOR_COMMAND_OFFSET_X_FLOAT_SLOT) - : 0.0f, - (operations.flags(index) & PhysicsCommandOperations.FLAG_OFFSET) != 0 - ? operations.floatAt(index, PhysicsCommandOperations.VECTOR_COMMAND_OFFSET_Y_FLOAT_SLOT) - : 0.0f, - (operations.flags(index) & PhysicsCommandOperations.FLAG_OFFSET) != 0 - ? operations.floatAt(index, PhysicsCommandOperations.VECTOR_COMMAND_OFFSET_Z_FLOAT_SLOT) - : 0.0f, - (operations.flags(index) & PhysicsCommandOperations.FLAG_TORQUE) != 0); - case PhysicsCommandOperations.CREATE_JOINT -> dispatchJoint(index, operations); - case PhysicsCommandOperations.DESTROY_JOINT -> destroyJoint( - operations.requiredObjectAt(index, - PhysicsCommandOperations.DESTROY_JOINT_KEY_OBJECT_SLOT, - JointKey.class)); - case PhysicsCommandOperations.DESTROY_JOINT_BETWEEN_BODIES -> destroyJointBetween( - (JointKey) operations.objectAt(index, - PhysicsCommandOperations.DESTROY_JOINT_BETWEEN_PREFERRED_KEY_OBJECT_SLOT), - operations.requiredObjectAt(index, - PhysicsCommandOperations.DESTROY_JOINT_BETWEEN_SPACE_ID_OBJECT_SLOT, - SpaceId.class), - operations.requiredObjectAt(index, - PhysicsCommandOperations.DESTROY_JOINT_BETWEEN_BODY_A_OBJECT_SLOT, - RigidBodyKey.class), - operations.requiredObjectAt(index, - PhysicsCommandOperations.DESTROY_JOINT_BETWEEN_BODY_B_OBJECT_SLOT, - RigidBodyKey.class)); - default -> throw new IllegalArgumentException("Unsupported physics command opcode " - + operations.opcode(index)); - } - } - - private void dispatchSpawn(int index, - @Nonnull PhysicsCommandOperations operations) { - spawnRigidBody(operations.requiredObjectAt(index, - PhysicsCommandOperations.SPAWN_BODY_KEY_OBJECT_SLOT, - RigidBodyKey.class), - operations.requiredObjectAt(index, - PhysicsCommandOperations.SPAWN_SPACE_ID_OBJECT_SLOT, - SpaceId.class), - operations.requiredObjectAt(index, - PhysicsCommandOperations.SPAWN_SHAPE_OBJECT_SLOT, - PhysicsShapeSpec.class), - operations.floatAt(index, PhysicsCommandOperations.SPAWN_MASS_FLOAT_SLOT), - operations.requiredObjectAt(index, - PhysicsCommandOperations.SPAWN_BODY_TYPE_OBJECT_SLOT, - PhysicsBodyType.class), - operations.floatAt(index, PhysicsCommandOperations.SPAWN_POSITION_X_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SPAWN_POSITION_Y_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.SPAWN_POSITION_Z_FLOAT_SLOT), - operations.requiredObjectAt(index, - PhysicsCommandOperations.SPAWN_SETTINGS_OBJECT_SLOT, - RigidBodySpawnSettings.class), - operations.requiredObjectAt(index, - PhysicsCommandOperations.SPAWN_KIND_OBJECT_SLOT, - PhysicsBodyKind.class), - operations.requiredObjectAt(index, - PhysicsCommandOperations.SPAWN_PERSISTENCE_MODE_OBJECT_SLOT, - PhysicsBodyPersistenceMode.class)); - } - - private void dispatchSpawnBatch(@Nonnull RigidBodySpawnBatch batch) { - for (int index = 0; index < batch.size(); index++) { - spawnRigidBody(batch.bodyKey(index), - batch.spaceId(index), - batch.shape(index), - batch.mass(index), - batch.bodyType(index), - batch.positionX(index), - batch.positionY(index), - batch.positionZ(index), - batch.settings(index), - batch.kind(index), - batch.persistenceMode(index)); - } - } - - private void dispatchSpawnTemplateBatch(@Nonnull RigidBodySpawnTemplateBatch batch) { - spawnRigidBodies(batch.size(), - batch.bodyKeyMostSignificantBits(), - batch.bodyKeyLeastSignificantBits(), - batch.spaceId(), - batch.shape(), - batch.mass(), - batch.bodyType(), - batch.positions(), - batch.settings(), - batch.kind(), - batch.persistenceMode()); - } - - private void dispatchJoint(int index, - @Nonnull PhysicsCommandOperations operations) { - createJoint(operations.requiredObjectAt(index, - PhysicsCommandOperations.CREATE_JOINT_JOINT_KEY_OBJECT_SLOT, - JointKey.class), - operations.requiredObjectAt(index, - PhysicsCommandOperations.CREATE_JOINT_SPACE_ID_OBJECT_SLOT, - SpaceId.class), - operations.requiredObjectAt(index, - PhysicsCommandOperations.CREATE_JOINT_BODY_A_OBJECT_SLOT, - RigidBodyKey.class), - operations.requiredObjectAt(index, - PhysicsCommandOperations.CREATE_JOINT_BODY_B_OBJECT_SLOT, - RigidBodyKey.class), - operations.requiredObjectAt(index, - PhysicsCommandOperations.CREATE_JOINT_TYPE_OBJECT_SLOT, - JointType.class), - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_ANCHOR_A_X_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_ANCHOR_A_Y_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_ANCHOR_A_Z_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_ANCHOR_B_X_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_ANCHOR_B_Y_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_ANCHOR_B_Z_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_AXIS_X_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_AXIS_Y_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_AXIS_Z_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_REST_LENGTH_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_STIFFNESS_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_DAMPING_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_LOWER_LIMIT_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_UPPER_LIMIT_FLOAT_SLOT), - (operations.flags(index) & PhysicsCommandOperations.FLAG_MOTOR_ENABLED) != 0, - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_MOTOR_TARGET_VELOCITY_FLOAT_SLOT), - operations.floatAt(index, PhysicsCommandOperations.CREATE_JOINT_MOTOR_MAX_FORCE_FLOAT_SLOT)); - } - - @Nonnull - public R query(@Nonnull PhysicsQuery query) { - Objects.requireNonNull(query, "query"); - Object result = switch (query) { - case RaycastClosestQuery raycast -> raycastClosest(raycast); - case RaycastClosestBatchQuery raycasts -> raycastClosestBatch(raycasts); - case RaycastAllQuery raycast -> raycastAll(raycast); - case SpaceBodyCountQuery count -> spaceBodyCount(count); - case SpaceSummaryQuery summary -> spaceSummary(summary); - case CcdSupportQuery ignored -> ccdSupported(); - case UnsupportedCcdSpacesQuery ignored -> unsupportedCcdSpaces(); - case SolverCapabilityQuery solver -> solverCapability(solver); - case RigidBodyStateQuery state -> rigidBodyState(state); - case RuntimeJointCountQuery ignored -> runtimeJointCount(); - }; - @SuppressWarnings("unchecked") - R typed = (R) result; - return typed; - } - - @Nonnull - public R queryInternal(@Nonnull PhysicsInternalQuery query) { - Objects.requireNonNull(query, "query"); - Object result = switch (query) { - case BenchmarkSpaceStatsQuery stats -> benchmarkSpaceStats(stats); - case PhysicsDebugContactsQuery contacts -> debugContacts(contacts); - case PhysicsDebugJointsQuery joints -> debugJoints(joints); - case PhysicsSpaceRuntimeStatsQuery stats -> physicsSpaceRuntimeStats(stats); - case WorldCollisionPrewarmEnvelopeQuery prewarm -> prewarmWorldCollisionEnvelope(prewarm); - }; - @SuppressWarnings("unchecked") - R typed = (R) result; - return typed; - } - - @Override - public void spawnRigidBody(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - float positionX, - float positionY, - float positionZ, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - if (runtime.getRegistration(bodyKey) != null) { - throw new IllegalArgumentException("Rigid body key=" + bodyKey - + " is already registered"); - } - PhysicsSpaceBinding space = requireSpace(spaceId); - spawnRigidBody(space, - bodyKey, - spaceId, - shape, - mass, - bodyType, - positionX, - positionY, - positionZ, - settings, - kind, - persistenceMode); - } - - @Override - public void spawnRigidBodies(int bodyCount, - @Nonnull long[] bodyKeyMostSignificantBits, - @Nonnull long[] bodyKeyLeastSignificantBits, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - @Nonnull float[] positions, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - PhysicsSpaceBinding space = requireSpace(spaceId); - for (int index = 0; index < bodyCount; index++) { - RigidBodyKey bodyKey = RigidBodyKey.of(bodyKeyMostSignificantBits[index], - bodyKeyLeastSignificantBits[index]); - if (runtime.getRegistration(bodyKey) != null) { - throw new IllegalArgumentException("Rigid body key=" + bodyKey - + " is already registered"); - } - int positionOffset = index * 3; - spawnRigidBody(space, - bodyKey, - spaceId, - shape, - mass, - bodyType, - positions[positionOffset], - positions[positionOffset + 1], - positions[positionOffset + 2], - settings, - kind, - persistenceMode); - } - } - - private void spawnRigidBody(@Nonnull PhysicsSpaceBinding space, - @Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - float positionX, - float positionY, - float positionZ, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - BackendBodyHandle backendBodyHandle = createRuntimeBody(space, - shape, - mass, - bodyType, - positionX, - positionY, - positionZ); - applySpawnSettings(space, backendBodyHandle, settings); - runtime.addBodyOnOwner(bodyKey, - spaceId, - backendBodyHandle, - kind, - persistenceMode); - } - - private void applySpawnSettings(@Nonnull PhysicsSpaceBinding space, - @Nonnull BackendBodyHandle backendBodyHandle, - @Nonnull RigidBodySpawnSettings settings) { - long backendBodyId = backendBodyHandle.value(); - if (settings.hasFriction()) { - space.runtime().setBodyFriction(space.backendSpaceHandle().value(), backendBodyId, settings.friction()); - } - if (settings.hasRestitution()) { - space.runtime().setBodyRestitution(space.backendSpaceHandle().value(), backendBodyId, settings.restitution()); - } - if (settings.hasLinearDamping()) { - space.runtime().setBodyDamping(space.backendSpaceHandle().value(), - backendBodyId, - settings.linearDamping(), - settings.hasAngularDamping() ? settings.angularDamping() : 0.0f); - } else if (settings.hasAngularDamping()) { - space.runtime().setBodyDamping(space.backendSpaceHandle().value(), backendBodyId, 0.0f, settings.angularDamping()); - } - if (settings.hasCollisionFilter()) { - space.runtime() - .setBodyCollisionFilter(space.backendSpaceHandle().value(), - backendBodyId, - settings.collisionGroup(), - settings.collisionMask()); - } - if (settings.hasSensor()) { - space.runtime().setBodySensor(space.backendSpaceHandle().value(), backendBodyId, settings.sensor()); - } - } - - @Nonnull - private BackendBodyHandle createRuntimeBody(@Nonnull PhysicsSpaceBinding space, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - float positionX, - float positionY, - float positionZ) { - long backendBodyId = space.runtime().createBody(space.backendSpaceHandle().value(), - BackendRuntimeCodes.shapeTypeCode(shape.type()), - shape.halfExtentX(), - shape.halfExtentY(), - shape.halfExtentZ(), - shape.radius(), - shape.halfHeight(), - BackendRuntimeCodes.axisCode(shape.axis()), - shape.groundY(), - mass, - BackendRuntimeCodes.bodyTypeCode(bodyType), - positionX, - positionY, - positionZ, - 0.0f, - 0.0f, - 0.0f, - 1.0f); - return new BackendBodyHandle(backendBodyId); - } - - @Override - public void destroyRigidBody(@Nonnull RigidBodyKey bodyKey) { - runtime.destroyBody(bodyKey); - } - - @Override - public void setSpaceGravity(@Nonnull SpaceId spaceId, - float x, - float y, - float z) { - PhysicsSpaceBinding space = requireSpace(spaceId); - space.runtime().setGravity(space.backendSpaceHandle().value(), x, y, z); - } - - @Override - public void setRigidBodyTransform(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ, - float rotationX, - float rotationY, - float rotationZ, - float rotationW, - boolean activate) { - PhysicsBodyRegistration registration = requireBodyRegistration(bodyKey); - PhysicsSpaceBinding space = requireSpace(registration.spaceId()); - space.runtime().setBodyTransform(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value(), - positionX, - positionY, - positionZ, - rotationX, - rotationY, - rotationZ, - rotationW); - if (activate) { - space.runtime().activateBody(space.backendSpaceHandle().value(), registration.backendBodyHandle().value()); - } - } - - @Override - public void setRigidBodyPosition(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ, - boolean activate) { - PhysicsBodyRegistration registration = requireBodyRegistration(bodyKey); - PhysicsSpaceBinding space = requireSpace(registration.spaceId()); - space.runtime().setBodyPosition(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value(), - positionX, - positionY, - positionZ); - if (activate) { - space.runtime().activateBody(space.backendSpaceHandle().value(), registration.backendBodyHandle().value()); - } - } - - @Override - public void setRigidBodyVelocity(@Nonnull RigidBodyKey bodyKey, - float linearX, - float linearY, - float linearZ, - float angularX, - float angularY, - float angularZ, - boolean activate) { - PhysicsBodyRegistration registration = requireBodyRegistration(bodyKey); - PhysicsSpaceBinding space = requireSpace(registration.spaceId()); - space.runtime().setBodyVelocity(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value(), - linearX, - linearY, - linearZ, - angularX, - angularY, - angularZ); - if (activate) { - space.runtime().activateBody(space.backendSpaceHandle().value(), registration.backendBodyHandle().value()); - } - } - - @Override - public void setRigidBodyType(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodyType bodyType, - boolean activate) { - PhysicsBodyRegistration registration = requireBodyRegistration(bodyKey); - PhysicsSpaceBinding space = requireSpace(registration.spaceId()); - space.runtime().setBodyType(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value(), - BackendRuntimeCodes.bodyTypeCode(bodyType)); - if (activate) { - space.runtime().activateBody(space.backendSpaceHandle().value(), registration.backendBodyHandle().value()); - } - } - - @Override - public void activateRigidBody(@Nonnull RigidBodyKey bodyKey) { - PhysicsBodyRegistration registration = requireBodyRegistration(bodyKey); - PhysicsSpaceBinding space = requireSpace(registration.spaceId()); - space.runtime().activateBody(space.backendSpaceHandle().value(), registration.backendBodyHandle().value()); - } - - @Override - public void applyRigidBodyImpulse(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z, - boolean hasOffset, - float offsetX, - float offsetY, - float offsetZ, - boolean torque) { - PhysicsBodyRegistration registration = requireBodyRegistration(bodyKey); - PhysicsSpaceBinding space = requireSpace(registration.spaceId()); - space.runtime().applyBodyImpulse(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value(), - x, - y, - z, - hasOffset, - offsetX, - offsetY, - offsetZ, - torque); - space.runtime().activateBody(space.backendSpaceHandle().value(), registration.backendBodyHandle().value()); - } - - @Override - public void applyRigidBodyForce(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z, - boolean hasOffset, - float offsetX, - float offsetY, - float offsetZ, - boolean torque) { - PhysicsBodyRegistration registration = requireBodyRegistration(bodyKey); - PhysicsSpaceBinding space = requireSpace(registration.spaceId()); - space.runtime().applyBodyForce(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value(), - x, - y, - z, - hasOffset, - offsetX, - offsetY, - offsetZ, - torque); - space.runtime().activateBody(space.backendSpaceHandle().value(), registration.backendBodyHandle().value()); - } - - @Override - public void createJoint(@Nonnull JointKey jointKey, - @Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyAKey, - @Nonnull RigidBodyKey bodyBKey, - @Nonnull JointType type, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ, - float restLength, - float stiffness, - float damping, - float lowerLimit, - float upperLimit, - boolean motorEnabled, - float motorTargetVelocity, - float motorMaxForce) { - PhysicsSpaceBinding space = requireSpace(spaceId); - PhysicsBodyRegistration bodyA = requireBodyRegistration(bodyAKey); - PhysicsBodyRegistration bodyB = requireBodyRegistration(bodyBKey); - if (!bodyA.spaceId().equals(spaceId) || !bodyB.spaceId().equals(spaceId)) { - throw new IllegalArgumentException("Joint bodies must both be registered in space " + spaceId); - } - BackendJointHandle backendJointHandle = new BackendJointHandle(space.runtime().createJoint( - space.backendSpaceHandle().value(), - toRuntimeJointTypeCode(type), - bodyA.backendBodyHandle().value(), - bodyB.backendBodyHandle().value(), - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - axisX, - axisY, - axisZ, - restLength, - stiffness, - damping, - lowerLimit, - upperLimit, - motorEnabled, - motorTargetVelocity, - motorMaxForce)); - runtime.addJointOnOwner(jointKey, - spaceId, - backendJointHandle, - bodyAKey, - bodyBKey, - type, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - axisX, - axisY, - axisZ, - restLength, - stiffness, - damping, - lowerLimit, - upperLimit, - motorEnabled, - motorTargetVelocity, - motorMaxForce); - } - - @Override - public void destroyJoint(@Nonnull JointKey jointKey) { - runtime.removeJoint(jointKey); - } - - @Override - public void destroyJointBetween(@Nullable JointKey preferredJointKey, - @Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB) { - if (preferredJointKey != null && runtime.removeJoint(preferredJointKey)) { - return; - } - PhysicsJointRegistration registration = runtime.findJointBetween(spaceId, bodyA, bodyB); - if (registration != null) { - runtime.removeJoint(registration.jointKey()); - } - } - - @Nonnull - private Optional raycastClosest(@Nonnull RaycastClosestQuery query) { - PhysicsSpaceBinding space = requireSpace(query.spaceId()); - RayHitCapture hit = new RayHitCapture(); - Vector3f from = query.from(); - Vector3f to = query.to(); - boolean hitFound = space.runtime().raycastClosest(space.backendSpaceHandle().value(), - from.x, - from.y, - from.z, - to.x, - to.y, - to.z, - hit); - return hitFound && hit.captured ? Optional.of(toView(hit)) : Optional.empty(); - } - - @Nonnull - private RaycastClosestBatchResult raycastClosestBatch(@Nonnull RaycastClosestBatchQuery query) { - PhysicsSpaceBinding space = requireSpace(query.spaceId()); - int rayCount = query.rayCount(); - RaycastHitView[] hits = new RaycastHitView[rayCount]; - Vector3f from = new Vector3f(); - Vector3f to = new Vector3f(); - for (int index = 0; index < rayCount; index++) { - RaycastSegment ray = query.ray(index); - ray.copyFrom(from); - ray.copyTo(to); - int rayIndex = index; - space.runtime().raycastClosest(space.backendSpaceHandle().value(), - from.x, - from.y, - from.z, - to.x, - to.y, - to.z, - (bodyId, - pointX, - pointY, - pointZ, - normalX, - normalY, - normalZ, - fraction, - distance) -> hits[rayIndex] = toView(bodyId, - pointX, - pointY, - pointZ, - normalX, - normalY, - normalZ, - fraction, - distance)); - } - return new RaycastClosestBatchResult(hits); - } - - @Nonnull - private List raycastAll(@Nonnull RaycastAllQuery query) { - PhysicsSpaceBinding space = requireSpace(query.spaceId()); - Vector3f from = query.from(); - Vector3f to = query.to(); - List views = new ArrayList<>(); - space.runtime().raycastAll(space.backendSpaceHandle().value(), - from.x, - from.y, - from.z, - to.x, - to.y, - to.z, - (bodyId, - pointX, - pointY, - pointZ, - normalX, - normalY, - normalZ, - fraction, - distance) -> views.add(toView(bodyId, - pointX, - pointY, - pointZ, - normalX, - normalY, - normalZ, - fraction, - distance))); - if (views.isEmpty()) { - return List.of(); - } - return List.copyOf(views); - } - - private int spaceBodyCount(@Nonnull SpaceBodyCountQuery query) { - PhysicsSpaceBinding space = runtime.getSpaceBinding(query.spaceId()); - return space != null ? space.runtime().bodyCount(space.backendSpaceHandle().value()) : 0; - } - - @Nonnull - private List spaceSummary(@Nonnull SpaceSummaryQuery query) { - List summaries = new ArrayList<>(); - if (query.spaceId() != null) { - PhysicsSpaceBinding space = runtime.getSpaceBinding(query.spaceId()); - if (space != null) { - summaries.add(summary(space)); - } - return List.copyOf(summaries); - } - for (PhysicsSpaceBinding space : runtime.getSpaceBindings()) { - summaries.add(summary(space)); - } - return List.copyOf(summaries); - } - - private boolean ccdSupported() { - for (PhysicsSpaceBinding space : runtime.getSpaceBindings()) { - if (space.runtime().supportsContinuousCollision(space.backendSpaceHandle().value())) { - return true; - } - } - return false; - } - - private int runtimeJointCount() { - int count = 0; - for (PhysicsSpaceBinding space : runtime.getSpaceBindings()) { - count += space.runtime().jointCount(space.backendSpaceHandle().value()); - } - return count; - } - - @Nonnull - private PhysicsSpaceRuntimeStatsView physicsSpaceRuntimeStats( - @Nonnull PhysicsSpaceRuntimeStatsQuery query) { - PhysicsSpaceBinding space = requireSpace(query.spaceId()); - WorldVoxelCollisionCache cache = runtime.worldCollisionCache(); - PhysicsSpaceRuntimeStatsAccumulator stats = new PhysicsSpaceRuntimeStatsAccumulator(); - for (PhysicsBodyRegistration registration : runtime.getBodyRegistrations()) { - if (!registration.spaceId().equals(query.spaceId())) { - continue; - } - PhysicsBodySnapshot snapshot = PhysicsBodySnapshots.read(space, registration.backendBodyHandle().value()); - if (snapshot != null) { - classifyRuntimeBody(stats, cache, space, registration, snapshot); - } - } - stats.worldCollisionBodies += cache.bodyCount(); - stats.joints = space.runtime().jointCount(space.backendSpaceHandle().value()); - stats.contacts = space.runtime().contactCount(space.backendSpaceHandle().value()); - space.runtime().runtimeStats(space.backendSpaceHandle().value(), - (bodyCount, - colliderCount, - activeBodyCount, - contactPairCount, - contactManifoldCount, - contactPointCount, - dynamicDynamicContactPairCount, - terrainContactPairCount, - activeIslandCount, - jointCount, - available) -> { - if (!available) { - return; - } - stats.runtimeStatsAvailable = true; - stats.runtimeBodyCount = bodyCount; - stats.runtimeColliderCount = colliderCount; - stats.runtimeActiveBodyCount = activeBodyCount; - stats.runtimeContactPairCount = contactPairCount; - stats.runtimeContactManifoldCount = contactManifoldCount; - stats.runtimeContactPointCount = contactPointCount; - stats.runtimeDynamicDynamicContactPairCount = dynamicDynamicContactPairCount; - stats.runtimeTerrainContactPairCount = terrainContactPairCount; - stats.runtimeActiveIslandCount = activeIslandCount; - stats.runtimeJointCount = jointCount; - }); - return stats.toView(); - } - - private void classifyRuntimeBody(@Nonnull PhysicsSpaceRuntimeStatsAccumulator stats, - @Nonnull WorldVoxelCollisionCache cache, - @Nonnull PhysicsSpaceBinding space, - @Nonnull PhysicsBodyRegistration registration, - @Nonnull PhysicsBodySnapshot snapshot) { - stats.bodies++; - if (snapshot.isDynamic()) { - stats.dynamicBodies++; - if (snapshot.sleeping()) { - stats.sleepingDynamicBodies++; - } else { - stats.awakeDynamicBodies++; - } - } else if (snapshot.isKinematic()) { - stats.kinematicBodies++; - } else { - stats.staticBodies++; - } - - if (registration.kind() == PhysicsBodyKind.BODY) { - if (runtime.hasBodyAttachments(registration.bodyKey())) { - stats.entityOwnedBodies++; - } else { - stats.detachedBodies++; - } - return; - } - if (registration.kind() == PhysicsBodyKind.WORLD_COLLISION) { - stats.worldCollisionBodies++; - return; - } - if (snapshot.shapeType() == ShapeType.PLANE) { - stats.planeBodies++; - return; - } - if (cache.containsBody(space.spaceId(), registration.backendBodyHandle().value())) { - stats.worldCollisionBodies++; - return; - } - stats.rawBodies++; - } - - @Nonnull - private BenchmarkSpaceStatsView benchmarkSpaceStats(@Nonnull BenchmarkSpaceStatsQuery query) { - PhysicsSpaceBinding space = requireSpace(query.spaceId()); - WorldVoxelCollisionCache cache = runtime.worldCollisionCache(); - BenchmarkSpaceStatsAccumulator stats = new BenchmarkSpaceStatsAccumulator(); - for (PhysicsBodyRegistration registration : runtime.getBodyRegistrations()) { - if (!registration.spaceId().equals(query.spaceId())) { - continue; - } - PhysicsBodySnapshot snapshot = PhysicsBodySnapshots.read(space, registration.backendBodyHandle().value()); - if (snapshot != null) { - classifyBenchmarkBody(stats, - cache, - space, - registration, - snapshot, - query); - } - } - int cachedWorldCollisionBodies = cache.bodyCount(space.spaceId()); - stats.bodies += cachedWorldCollisionBodies; - stats.worldCollisionBodies += cachedWorldCollisionBodies; - return stats.toView(); - } - - private void classifyBenchmarkBody(@Nonnull BenchmarkSpaceStatsAccumulator stats, - @Nonnull WorldVoxelCollisionCache cache, - @Nonnull PhysicsSpaceBinding space, - @Nonnull PhysicsBodyRegistration registration, - @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull BenchmarkSpaceStatsQuery query) { - stats.bodies++; - if (snapshot.isDynamic()) { - stats.dynamicBodies++; - Vector3f position = snapshot.position(); - stats.minDynamicBodyY = Math.min(stats.minDynamicBodyY, position.y); - stats.maxDynamicBodyY = Math.max(stats.maxDynamicBodyY, position.y); - if (position.y < query.groundY() - query.belowPlaneTolerance()) { - stats.belowPlaneBodies++; - } - if (query.includeTerrainProbe()) { - WorldVoxelCollisionCache.GroundProbe ground = cache.probeGround(space.spaceId(), - position.x, - position.z, - horizontalHalfExtent(snapshot)); - if (ground.found()) { - stats.terrainBaselineBodies++; - double bottomClearance = position.y - verticalHalfExtent(snapshot) - ground.topY(); - stats.minTerrainBottomClearance = - Math.min(stats.minTerrainBottomClearance, bottomClearance); - if (bottomClearance < -query.belowPlaneTolerance()) { - stats.belowTerrainBodies++; - } - } else { - stats.missingTerrainBaselineBodies++; - } - } - if (position.y < query.bodyWorldMinY()) { - stats.belowWorldMinBodies++; - } - if (position.y < query.bodyVoidY()) { - stats.belowVoidBodies++; - } - if (snapshot.sleeping()) { - stats.sleepingDynamicBodies++; - } else { - stats.awakeDynamicBodies++; - } - } - - if (registration.kind() == PhysicsBodyKind.BODY) { - if (!runtime.hasBodyAttachments(registration.bodyKey())) { - stats.detachedBodies++; - } - return; - } - if (snapshot.shapeType() == ShapeType.PLANE) { - return; - } - if (cache.containsBody(space.spaceId(), registration.backendBodyHandle().value())) { - stats.worldCollisionBodies++; - return; - } - stats.rawBodies++; - } - - @Nonnull - private WorldCollisionPrewarmStats prewarmWorldCollisionEnvelope( - @Nonnull WorldCollisionPrewarmEnvelopeQuery query) { - PhysicsSpaceBinding space = requireSpace(query.spaceId()); - WorldVoxelCollisionCache cache = runtime.worldCollisionCache(); - WorldCollisionBuildOptions buildOptions = - WorldCollisionBuildOptions.fromSettings(runtime.getLiveSpaceSettings(query.spaceId()) - .getWorldCollisionSettings()); - LongSet visitedSections = new LongOpenHashSet(); - Set visitedTargets = new ObjectOpenHashSet<>(); - BuildStats total = BuildStats.empty(); - for (int index = 0; index < Math.max(0, query.count()); index++) { - double positionX = query.originX() + (index % query.side()) * query.spacing(); - double positionY = query.originY(); - double positionZ = query.originZ() + ((double) index / query.side()) * query.spacing(); - total = total.plus(prewarmStreamingCollisionEnvelope(cache, - space, - query, - positionX, - positionY, - positionZ, - visitedSections, - visitedTargets, - buildOptions)); - } - return new WorldCollisionPrewarmStats(visitedSections.size(), worldCollisionStats(total)); - } - - @Nonnull - private BuildStats prewarmStreamingCollisionEnvelope( - @Nonnull WorldVoxelCollisionCache cache, - @Nonnull PhysicsSpaceBinding space, - @Nonnull WorldCollisionPrewarmEnvelopeQuery query, - double positionX, - double positionY, - double positionZ, - @Nonnull LongSet visitedSections, - @Nonnull Set visitedTargets, - @Nonnull WorldCollisionBuildOptions buildOptions) { - BuildStats total = BuildStats.empty(); - double halo = query.horizontalDriftHaloBlocks(); - for (int offsetX = -1; offsetX <= 1; offsetX++) { - for (int offsetZ = -1; offsetZ <= 1; offsetZ++) { - total = total.plus(prewarmStreamingCollisionEnvelopeAt(cache, - space, - query, - positionX + offsetX * halo, - positionY, - positionZ + offsetZ * halo, - visitedSections, - visitedTargets, - buildOptions)); - } - } - return total; - } - - @Nonnull - private BuildStats prewarmStreamingCollisionEnvelopeAt( - @Nonnull WorldVoxelCollisionCache cache, - @Nonnull PhysicsSpaceBinding space, - @Nonnull WorldCollisionPrewarmEnvelopeQuery query, - double positionX, - double positionY, - double positionZ, - @Nonnull LongSet visitedSections, - @Nonnull Set visitedTargets, - @Nonnull WorldCollisionBuildOptions buildOptions) { - double step = Math.max(1.0, query.radius() * 2.0); - double minCenterY = Math.min(positionY, query.fallEnvelopeMinY() + query.radius()); - BuildStats total = BuildStats.empty(); - double lastY = Double.NaN; - for (double y = positionY; y >= minCenterY; y -= step) { - total = total.plus(prewarmStreamingCollisionTarget(cache, - space, - query, - positionX, - y, - positionZ, - visitedSections, - visitedTargets, - buildOptions)); - lastY = y; - } - if (Double.isNaN(lastY) || lastY > minCenterY) { - total = total.plus(prewarmStreamingCollisionTarget(cache, - space, - query, - positionX, - minCenterY, - positionZ, - visitedSections, - visitedTargets, - buildOptions)); - } - return total; - } - - @Nonnull - private BuildStats prewarmStreamingCollisionTarget( - @Nonnull WorldVoxelCollisionCache cache, - @Nonnull PhysicsSpaceBinding space, - @Nonnull WorldCollisionPrewarmEnvelopeQuery query, - double centerX, - double centerY, - double centerZ, - @Nonnull LongSet visitedSections, - @Nonnull Set visitedTargets, - @Nonnull WorldCollisionBuildOptions buildOptions) { - StreamingPrewarmTarget target = streamingPrewarmTarget(centerX, - centerY, - centerZ, - query.radius()); - if (!visitedTargets.add(target)) { - return BuildStats.empty(); - } - return cache.ensureAround(query.world(), - space, - new Vector3d(centerX, centerY, centerZ), - query.radius(), - query.tick(), - null, - visitedSections, - null, - null, - buildOptions); - } - - @Nonnull - private static StreamingPrewarmTarget streamingPrewarmTarget(double centerX, - double centerY, - double centerZ, - int radius) { - int minX = (int) Math.floor(centerX) - radius; - int maxX = (int) Math.floor(centerX) + radius; - int minY = Math.max(0, (int) Math.floor(centerY) - radius); - int maxY = Math.min(ChunkUtil.HEIGHT_MINUS_1, (int) Math.floor(centerY) + radius); - int minZ = (int) Math.floor(centerZ) - radius; - int maxZ = (int) Math.floor(centerZ) + radius; - return new StreamingPrewarmTarget(ChunkUtil.chunkCoordinate(minX), - ChunkUtil.chunkCoordinate(maxX), - ChunkUtil.indexSection(minY), - ChunkUtil.indexSection(maxY), - ChunkUtil.chunkCoordinate(minZ), - ChunkUtil.chunkCoordinate(maxZ)); - } - - @Nonnull - private static WorldCollisionBuildStats worldCollisionStats(@Nonnull BuildStats stats) { - return new WorldCollisionBuildStats(stats.scannedBlocks(), - stats.solidBlocks(), - stats.culledInteriorBlocks(), - stats.fullCubeRuns(), - stats.detailBoxes(), - stats.colliderBodies(), - stats.removedBodies(), - stats.sectionsBuilt(), - stats.sectionsRebuilt(), - stats.voxelBodies()); - } - - private static double horizontalHalfExtent(@Nonnull PhysicsBodySnapshot snapshot) { - if (snapshot.shapeType() == ShapeType.BOX) { - Vector3f halfExtents = snapshot.boxHalfExtents(); - if (halfExtents != null) { - return Math.max(finitePositive(halfExtents.x), finitePositive(halfExtents.z)); - } - } - return Math.max(finitePositive(snapshot.sphereRadius()), finitePositive(snapshot.halfHeight())); - } - - private static double verticalHalfExtent(@Nonnull PhysicsBodySnapshot snapshot) { - if (snapshot.shapeType() == ShapeType.BOX) { - Vector3f halfExtents = snapshot.boxHalfExtents(); - if (halfExtents != null) { - return finitePositive(halfExtents.y); - } - } - if (snapshot.shapeType() == ShapeType.SPHERE) { - return finitePositive(snapshot.sphereRadius()); - } - return finitePositive(snapshot.halfHeight()) + finitePositive(snapshot.sphereRadius()); - } - - private static double finitePositive(float value) { - return Float.isFinite(value) && value > 0.0f ? value : 0.0; - } - - @Nonnull - private List debugContacts(@Nonnull PhysicsDebugContactsQuery query) { - int limit = Math.max(0, query.maxContacts()); - if (limit == 0) { - return List.of(); - } - - PhysicsSpaceBinding space = requireSpace(query.spaceId()); - double maxDistanceSquared = query.viewRadius() * query.viewRadius(); - List visible = new ArrayList<>(Math.min(limit, 64)); - space.runtime().contacts(space.backendSpaceHandle().value(), (bodyAId, - bodyBId, - pointAX, - pointAY, - pointAZ, - pointBX, - pointBY, - pointBZ, - normalBX, - normalBY, - normalBZ, - distance, - impulse) -> { - if (visible.size() >= limit) { - return; - } - - if (distanceSquared(pointBX, - pointBY, - pointBZ, - query.viewerX(), - query.viewerY(), - query.viewerZ()) > maxDistanceSquared) { - return; - } - - visible.add(toDebugContactView(pointBX, - pointBY, - pointBZ, - normalBX, - normalBY, - normalBZ, - impulse)); - }); - return List.copyOf(visible); - } - - @Nonnull - private List debugJoints(@Nonnull PhysicsDebugJointsQuery query) { - int limit = Math.max(0, query.maxJoints()); - if (limit == 0) { - return List.of(); - } - - double maxDistanceSquared = query.viewRadius() * query.viewRadius(); - List visible = new ArrayList<>(Math.min(limit, 64)); - for (PhysicsJointRegistration joint : runtime.getJointRegistrations()) { - if (!joint.spaceId().equals(query.spaceId())) { - continue; - } - if (visible.size() >= limit) { - break; - } - - PhysicsDebugJointView view = toDebugJointView(joint); - double midpointX = (view.anchorAX() + view.anchorBX()) * 0.5; - double midpointY = (view.anchorAY() + view.anchorBY()) * 0.5; - double midpointZ = (view.anchorAZ() + view.anchorBZ()) * 0.5; - if (distanceSquared(midpointX, - midpointY, - midpointZ, - query.viewerX(), - query.viewerY(), - query.viewerZ()) > maxDistanceSquared) { - continue; - } - - visible.add(view); - } - return List.copyOf(visible); - } - - @Nonnull - private static PhysicsDebugContactView toDebugContactView(float pointX, - float pointY, - float pointZ, - float normalX, - float normalY, - float normalZ, - float impulse) { - Vector3f normal = new Vector3f(normalX, normalY, normalZ); - if (normal.lengthSquared() <= 0.0f) { - return new PhysicsDebugContactView(pointX, - pointY, - pointZ, - false, - 0.0f, - 0.0f, - 0.0f); - } - - float magnitude = Math.max(CONTACT_NORMAL_SCALE, Math.abs(impulse) * 0.05f); - normal.normalize().mul(magnitude); - return new PhysicsDebugContactView(pointX, - pointY, - pointZ, - true, - normal.x, - normal.y, - normal.z); - } - - @Nonnull - private PhysicsDebugJointView toDebugJointView(@Nonnull PhysicsJointRegistration joint) { - PhysicsBodySnapshot bodyA = runtime.getBodySnapshot(joint.bodyA()); - PhysicsBodySnapshot bodyB = runtime.getBodySnapshot(joint.bodyB()); - Vector3f anchorA = worldAnchor(bodyA, joint.anchorAX(), joint.anchorAY(), joint.anchorAZ()); - Vector3f anchorB = worldAnchor(bodyB, joint.anchorBX(), joint.anchorBY(), joint.anchorBZ()); - Vector3f axis = new Vector3f(joint.axisX(), joint.axisY(), joint.axisZ()); - if (axis.lengthSquared() <= 0.0f) { - return new PhysicsDebugJointView(anchorA.x, - anchorA.y, - anchorA.z, - anchorB.x, - anchorB.y, - anchorB.z, - false, - 0.0f, - 0.0f, - 0.0f); - } - - Vector3f worldAxis = new Vector3f(axis).normalize().mul(JOINT_AXIS_SCALE); - bodyA.rotation().transform(worldAxis); - return new PhysicsDebugJointView(anchorA.x, - anchorA.y, - anchorA.z, - anchorB.x, - anchorB.y, - anchorB.z, - true, - worldAxis.x, - worldAxis.y, - worldAxis.z); - } - - @Nonnull - private static Vector3f worldAnchor(@Nonnull PhysicsBodySnapshot body, - float localX, - float localY, - float localZ) { - Vector3f anchor = new Vector3f(localX, localY, localZ); - body.rotation().transform(anchor); - Vector3f position = body.position(); - return anchor.add(position); - } - - private static double distanceSquared(double x, - double y, - double z, - double targetX, - double targetY, - double targetZ) { - double dx = x - targetX; - double dy = y - targetY; - double dz = z - targetZ; - return dx * dx + dy * dy + dz * dz; - } - - private static final class BenchmarkSpaceStatsAccumulator { - - private int bodies; - private int dynamicBodies; - private int awakeDynamicBodies; - private int sleepingDynamicBodies; - private int detachedBodies; - private int rawBodies; - private int worldCollisionBodies; - private int belowPlaneBodies; - private int belowTerrainBodies; - private int belowWorldMinBodies; - private int belowVoidBodies; - private int terrainBaselineBodies; - private int missingTerrainBaselineBodies; - private double minTerrainBottomClearance = Double.POSITIVE_INFINITY; - private double minDynamicBodyY = Double.POSITIVE_INFINITY; - private double maxDynamicBodyY = Double.NEGATIVE_INFINITY; - - @Nonnull - private BenchmarkSpaceStatsView toView() { - return new BenchmarkSpaceStatsView(bodies, - dynamicBodies, - awakeDynamicBodies, - sleepingDynamicBodies, - detachedBodies, - rawBodies, - worldCollisionBodies, - belowPlaneBodies, - belowTerrainBodies, - belowWorldMinBodies, - belowVoidBodies, - terrainBaselineBodies, - missingTerrainBaselineBodies, - Double.isFinite(minTerrainBottomClearance) ? (float) minTerrainBottomClearance : Float.NaN, - Double.isFinite(minDynamicBodyY) ? (float) minDynamicBodyY : Float.NaN, - Double.isFinite(maxDynamicBodyY) ? (float) maxDynamicBodyY : Float.NaN); - } - } - - private static final class PhysicsSpaceRuntimeStatsAccumulator { - - private int bodies; - private int dynamicBodies; - private int awakeDynamicBodies; - private int sleepingDynamicBodies; - private int staticBodies; - private int kinematicBodies; - private int entityOwnedBodies; - private int detachedBodies; - private int worldCollisionBodies; - private int planeBodies; - private int rawBodies; - private int joints; - private int contacts; - private boolean runtimeStatsAvailable; - private int runtimeBodyCount; - private int runtimeColliderCount; - private int runtimeActiveBodyCount; - private int runtimeContactPairCount; - private int runtimeContactManifoldCount; - private int runtimeContactPointCount; - private int runtimeDynamicDynamicContactPairCount; - private int runtimeTerrainContactPairCount; - private int runtimeActiveIslandCount; - private int runtimeJointCount; - - @Nonnull - private PhysicsSpaceRuntimeStatsView toView() { - return new PhysicsSpaceRuntimeStatsView(bodies, - dynamicBodies, - awakeDynamicBodies, - sleepingDynamicBodies, - staticBodies, - kinematicBodies, - entityOwnedBodies, - detachedBodies, - worldCollisionBodies, - planeBodies, - rawBodies, - joints, - contacts, - runtimeStatsAvailable, - runtimeBodyCount, - runtimeColliderCount, - runtimeActiveBodyCount, - runtimeContactPairCount, - runtimeContactManifoldCount, - runtimeContactPointCount, - runtimeDynamicDynamicContactPairCount, - runtimeTerrainContactPairCount, - runtimeActiveIslandCount, - runtimeJointCount); - } - } - - private record StreamingPrewarmTarget(int minChunkX, - int maxChunkX, - int minSectionY, - int maxSectionY, - int minChunkZ, - int maxChunkZ) { - } - - @Nonnull - private Optional rigidBodyState(@Nonnull RigidBodyStateQuery query) { - PhysicsBodyRegistration registration = runtime.getRegistration(query.bodyKey()); - if (registration == null) { - return Optional.empty(); - } - PhysicsSpaceBinding space = requireSpace(registration.spaceId()); - PhysicsBodySnapshot snapshot = PhysicsBodySnapshots.read(space, registration.backendBodyHandle().value()); - if (snapshot == null) { - return Optional.empty(); - } - return Optional.of(new RigidBodyStateView(query.bodyKey(), - snapshot.bodyType(), - RigidBodyPose.of(snapshot.position(), snapshot.rotation()))); - } - - @Nonnull - private SolverCapabilitySummary solverCapability(@Nonnull SolverCapabilityQuery query) { - PhysicsSpaceBinding space = requireSpace(query.spaceId()); - return new SolverCapabilitySummary(space.spaceId(), - space.backendId().value(), - space.runtime().supportsSolverTuning(space.backendSpaceHandle().value()), - space.runtime().supportsActivationTuning(space.backendSpaceHandle().value())); - } - - @Nonnull - private List unsupportedCcdSpaces() { - List spaces = new ArrayList<>(); - for (PhysicsSpaceBinding space : runtime.getSpaceBindings()) { - if (space.runtime().supportsContinuousCollision(space.backendSpaceHandle().value())) { - continue; - } - spaces.add(summary(space)); - } - return List.copyOf(spaces); - } - - @Nonnull - private SpaceSummary summary(@Nonnull PhysicsSpaceBinding space) { - return new SpaceSummary(space.spaceId(), - space.backendId(), - space.runtime().bodyCount(space.backendSpaceHandle().value()), - space.runtime().jointCount(space.backendSpaceHandle().value())); - } - - @Nonnull - private RaycastHitView toView(@Nonnull RayHitCapture hit) { - return toView(hit.bodyId, - hit.pointX, - hit.pointY, - hit.pointZ, - hit.normalX, - hit.normalY, - hit.normalZ, - hit.fraction, - hit.distance); - } - - @Nonnull - private RaycastHitView toView(long bodyId, - float pointX, - float pointY, - float pointZ, - float normalX, - float normalY, - float normalZ, - float fraction, - float distance) { - PhysicsBodyRegistration registration = findBodyRegistration(bodyId); - RigidBodyKey bodyKey = registration != null ? registration.bodyKey() : null; - PhysicsBodySnapshot snapshot = registration != null - ? runtime.getBodySnapshot(registration.bodyKey()) - : null; - return new RaycastHitView(bodyKey, - snapshot != null ? snapshot.bodyType() : PhysicsBodyType.STATIC, - new Vector3f(pointX, pointY, pointZ), - new Vector3f(normalX, normalY, normalZ), - snapshot != null ? snapshot.shapeType() : ShapeType.BOX, - fraction, - distance); - } - - private static final class RayHitCapture implements BackendRayHitSink { - - private long bodyId; - private float pointX; - private float pointY; - private float pointZ; - private float normalX; - private float normalY; - private float normalZ; - private float fraction; - private float distance; - private boolean captured; - - @Override - public void accept(long bodyId, - float pointX, - float pointY, - float pointZ, - float normalX, - float normalY, - float normalZ, - float fraction, - float distance) { - this.bodyId = bodyId; - this.pointX = pointX; - this.pointY = pointY; - this.pointZ = pointZ; - this.normalX = normalX; - this.normalY = normalY; - this.normalZ = normalZ; - this.fraction = fraction; - this.distance = distance; - captured = true; - } - } - - @Nonnull - private PhysicsSpaceBinding requireSpace(@Nonnull SpaceId spaceId) { - PhysicsSpaceBinding space = runtime.getSpaceBinding(spaceId); - if (space == null) { - throw new IllegalArgumentException("Physics space id=" + spaceId + " is not registered"); - } - return space; - } - - @Nonnull - private PhysicsBodyRegistration requireBodyRegistration(@Nonnull RigidBodyKey bodyKey) { - PhysicsBodyRegistration registration = runtime.getRegistration(bodyKey); - if (registration == null) { - throw new IllegalArgumentException("Rigid body key=" + bodyKey + " is not registered"); - } - return registration; - } - - @Nullable - private PhysicsBodyRegistration findBodyRegistration(long backendBodyId) { - for (PhysicsBodyRegistration registration : runtime.getBodyRegistrations()) { - if (registration.backendBodyHandle().value() == backendBodyId) { - return registration; - } - } - return null; - } - - private static int toRuntimeJointTypeCode(@Nonnull JointType type) { - return switch (type) { - case FIXED -> BackendRuntimeCodes.JOINT_FIXED; - case POINT -> BackendRuntimeCodes.JOINT_POINT; - case HINGE -> BackendRuntimeCodes.JOINT_HINGE; - case SLIDER -> BackendRuntimeCodes.JOINT_SLIDER; - case SPRING -> BackendRuntimeCodes.JOINT_SPRING; - }; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/RecordedBodyCreationKeys.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/RecordedBodyCreationKeys.java deleted file mode 100644 index 25d353cf..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/RecordedBodyCreationKeys.java +++ /dev/null @@ -1,120 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation; - -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import java.util.Arrays; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Frozen created-body key metadata for one recorded command batch. - */ -public final class RecordedBodyCreationKeys { - - private static final RecordedBodyCreationKeys EMPTY = - new RecordedBodyCreationKeys(new long[0], new long[0], 0); - - @Nonnull - private final long[] mostSignificantBits; - @Nonnull - private final long[] leastSignificantBits; - private final int size; - - private RecordedBodyCreationKeys(@Nonnull long[] mostSignificantBits, - @Nonnull long[] leastSignificantBits, - int size) { - this.mostSignificantBits = mostSignificantBits; - this.leastSignificantBits = leastSignificantBits; - this.size = size; - } - - @Nonnull - public static RecordedBodyCreationKeys empty() { - return EMPTY; - } - - @Nonnull - public static Builder builder() { - return new Builder(); - } - - public boolean isEmpty() { - return size == 0; - } - - public int size() { - return size; - } - - @Nonnull - public RigidBodyKey bodyKey(int index) { - checkIndex(index); - return RigidBodyKey.of(mostSignificantBits[index], leastSignificantBits[index]); - } - - @Nullable - public RigidBodyKey singleBodyKey() { - return size == 1 ? bodyKey(0) : null; - } - - private void checkIndex(int index) { - if (index < 0 || index >= size) { - throw new IndexOutOfBoundsException(index); - } - } - - public static final class Builder { - - private long[] mostSignificantBits = new long[4]; - private long[] leastSignificantBits = new long[4]; - private int size; - - public void add(@Nonnull RigidBodyKey bodyKey) { - add(bodyKey.mostSignificantBits(), bodyKey.leastSignificantBits()); - } - - void add(long bodyKeyMostSignificantBits, - long bodyKeyLeastSignificantBits) { - ensureCapacity(size + 1); - mostSignificantBits[size] = bodyKeyMostSignificantBits; - leastSignificantBits[size] = bodyKeyLeastSignificantBits; - size++; - } - - public void addAll(@Nonnull long[] bodyKeyMostSignificantBits, - @Nonnull long[] bodyKeyLeastSignificantBits, - int count) { - if (count <= 0) { - return; - } - ensureCapacity(size + count); - System.arraycopy(bodyKeyMostSignificantBits, 0, mostSignificantBits, size, count); - System.arraycopy(bodyKeyLeastSignificantBits, 0, leastSignificantBits, size, count); - size += count; - } - - @Nonnull - public RecordedBodyCreationKeys build() { - if (size == 0) { - return empty(); - } - return new RecordedBodyCreationKeys( - mostSignificantBits.length == size - ? mostSignificantBits - : Arrays.copyOf(mostSignificantBits, size), - leastSignificantBits.length == size - ? leastSignificantBits - : Arrays.copyOf(leastSignificantBits, size), - size); - } - - private void ensureCapacity(int required) { - if (required <= mostSignificantBits.length) { - return; - } - int nextCapacity = Math.max(required, - mostSignificantBits.length + (mostSignificantBits.length >> 1)); - mostSignificantBits = Arrays.copyOf(mostSignificantBits, nextCapacity); - leastSignificantBits = Arrays.copyOf(leastSignificantBits, nextCapacity); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/batch/RecordedPhysicsCommandBatch.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/batch/RecordedPhysicsCommandBatch.java deleted file mode 100644 index dc6e318e..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/batch/RecordedPhysicsCommandBatch.java +++ /dev/null @@ -1,114 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.batch; - -import dev.hytalemodding.impulse.core.internal.simulation.PhysicsCommandOperations; -import dev.hytalemodding.impulse.core.internal.simulation.RecordedBodyCreationKeys; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandBatch; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandMetadata; -import java.util.Objects; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Internal frozen command payload plus the public metadata view returned to plugins. - */ -public final class RecordedPhysicsCommandBatch { - - @Nonnull - private final PhysicsCommandMetadata metadata; - private final long commandWorldEpoch; - @Nonnull - private final PhysicsCommandOperations operations; - @Nonnull - private final PhysicsCommandBatch publicBatch; - @Nonnull - private final RecordedBodyCreationKeys bodyCreationKeys; - private final int bodyKeyReferenceCount; - @Nullable - private final RigidBodyKey firstBodyKey; - private final int jointKeyReferenceCount; - @Nullable - private final JointKey firstJointKey; - - public RecordedPhysicsCommandBatch(@Nonnull PhysicsCommandMetadata metadata, - long commandWorldEpoch, - @Nonnull PhysicsCommandOperations operations) { - this.metadata = Objects.requireNonNull(metadata, "metadata"); - this.commandWorldEpoch = Math.max(0L, commandWorldEpoch); - this.operations = Objects.requireNonNull(operations, "operations"); - this.publicBatch = new PhysicsCommandBatch(metadata, operations.size()); - this.bodyCreationKeys = operations.bodyCreationKeys(); - PhysicsCommandOperations.EntityReferences references = operations.entityReferences(); - this.bodyKeyReferenceCount = references.bodyKeyReferenceCount(); - this.firstBodyKey = references.firstBodyKey(); - this.jointKeyReferenceCount = references.jointKeyReferenceCount(); - this.firstJointKey = references.firstJointKey(); - } - - @Nonnull - public PhysicsCommandMetadata metadata() { - return metadata; - } - - public long commandWorldEpoch() { - return commandWorldEpoch; - } - - public int commandCount() { - return operations.size(); - } - - @Nonnull - public PhysicsCommandOperations operations() { - return operations; - } - - @Nonnull - public PhysicsCommandBatch publicBatch() { - return publicBatch; - } - - /** - * Returns whether the frozen batch can create rigid bodies. - */ - public boolean hasBodyCreationCommands() { - return !bodyCreationKeys.isEmpty(); - } - - /** - * Returns the exact created body key when this batch creates exactly one rigid body. - */ - @Nullable - public RigidBodyKey singleSpawnBodyKey() { - return bodyCreationKeys.singleBodyKey(); - } - - public int bodyCreationKeyCount() { - return bodyCreationKeys.size(); - } - - @Nonnull - public RigidBodyKey bodyCreationKey(int index) { - return bodyCreationKeys.bodyKey(index); - } - - public int bodyKeyReferenceCount() { - return bodyKeyReferenceCount; - } - - @Nullable - public RigidBodyKey firstBodyKey() { - return firstBodyKey; - } - - public int jointKeyReferenceCount() { - return jointKeyReferenceCount; - } - - @Nullable - public JointKey firstJointKey() { - return firstJointKey; - } - -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/batch/RigidBodySpawnBatch.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/batch/RigidBodySpawnBatch.java deleted file mode 100644 index fb1f5b63..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/batch/RigidBodySpawnBatch.java +++ /dev/null @@ -1,222 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.batch; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandContext; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import java.util.Arrays; -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Packed execution encoding for repeated rigid body spawns. - * - *

This is an internal recorded-command representation. Plugin code should record - * through {@link PhysicsCommandContext#spawnBodies} instead of depending on this type.

- */ -public final class RigidBodySpawnBatch { - - private static final int OBJECT_STRIDE = 6; - private static final int FLOAT_STRIDE = 4; - - private long[] bodyKeyMostSignificantBits; - private long[] bodyKeyLeastSignificantBits; - private Object[] objects; - private float[] floats; - private int size; - - public RigidBodySpawnBatch(int expectedBodies) { - int capacity = Math.max(1, expectedBodies); - bodyKeyMostSignificantBits = new long[capacity]; - bodyKeyLeastSignificantBits = new long[capacity]; - objects = new Object[capacity * OBJECT_STRIDE]; - floats = new float[capacity * FLOAT_STRIDE]; - } - - private RigidBodySpawnBatch(@Nonnull long[] bodyKeyMostSignificantBits, - @Nonnull long[] bodyKeyLeastSignificantBits, - @Nonnull Object[] objects, - @Nonnull float[] floats, - int size) { - this.bodyKeyMostSignificantBits = bodyKeyMostSignificantBits; - this.bodyKeyLeastSignificantBits = bodyKeyLeastSignificantBits; - this.objects = objects; - this.floats = floats; - this.size = size; - } - - public void add(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - float positionX, - float positionY, - float positionZ, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - Objects.requireNonNull(bodyKey, "bodyKey"); - add(bodyKey.mostSignificantBits(), - bodyKey.leastSignificantBits(), - spaceId, - shape, - mass, - bodyType, - positionX, - positionY, - positionZ, - settings, - kind, - persistenceMode); - } - - public void add(long bodyKeyMostSignificantBits, - long bodyKeyLeastSignificantBits, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - float positionX, - float positionY, - float positionZ, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - ensureCapacity(size + 1); - this.bodyKeyMostSignificantBits[size] = bodyKeyMostSignificantBits; - this.bodyKeyLeastSignificantBits[size] = bodyKeyLeastSignificantBits; - int objectOffset = size * OBJECT_STRIDE; - objects[objectOffset] = Objects.requireNonNull(spaceId, "spaceId"); - objects[objectOffset + 1] = Objects.requireNonNull(shape, "shape"); - objects[objectOffset + 2] = Objects.requireNonNull(bodyType, "bodyType"); - objects[objectOffset + 3] = Objects.requireNonNull(settings, "settings"); - objects[objectOffset + 4] = Objects.requireNonNull(kind, "kind"); - objects[objectOffset + 5] = Objects.requireNonNull(persistenceMode, "persistenceMode"); - - int floatOffset = size * FLOAT_STRIDE; - floats[floatOffset] = mass; - floats[floatOffset + 1] = positionX; - floats[floatOffset + 2] = positionY; - floats[floatOffset + 3] = positionZ; - size++; - } - - @Nonnull - public RigidBodySpawnBatch freeze() { - int objectSize = size * OBJECT_STRIDE; - int floatSize = size * FLOAT_STRIDE; - return new RigidBodySpawnBatch( - bodyKeyMostSignificantBits.length == size - ? bodyKeyMostSignificantBits - : Arrays.copyOf(bodyKeyMostSignificantBits, size), - bodyKeyLeastSignificantBits.length == size - ? bodyKeyLeastSignificantBits - : Arrays.copyOf(bodyKeyLeastSignificantBits, size), - objects.length == objectSize ? objects : Arrays.copyOf(objects, objectSize), - floats.length == floatSize ? floats : Arrays.copyOf(floats, floatSize), - size); - } - - public int size() { - return size; - } - - @Nonnull - public RigidBodyKey bodyKey(int index) { - checkIndex(index); - return RigidBodyKey.of(bodyKeyMostSignificantBits[index], bodyKeyLeastSignificantBits[index]); - } - - @Nonnull - public long[] bodyKeyMostSignificantBits() { - return bodyKeyMostSignificantBits; - } - - @Nonnull - public long[] bodyKeyLeastSignificantBits() { - return bodyKeyLeastSignificantBits; - } - - @Nonnull - public SpaceId spaceId(int index) { - return object(index, 0, SpaceId.class); - } - - @Nonnull - public PhysicsShapeSpec shape(int index) { - return object(index, 1, PhysicsShapeSpec.class); - } - - @Nonnull - public PhysicsBodyType bodyType(int index) { - return object(index, 2, PhysicsBodyType.class); - } - - @Nonnull - public RigidBodySpawnSettings settings(int index) { - return object(index, 3, RigidBodySpawnSettings.class); - } - - @Nonnull - public PhysicsBodyKind kind(int index) { - return object(index, 4, PhysicsBodyKind.class); - } - - @Nonnull - public PhysicsBodyPersistenceMode persistenceMode(int index) { - return object(index, 5, PhysicsBodyPersistenceMode.class); - } - - public float mass(int index) { - return floatAt(index, 0); - } - - public float positionX(int index) { - return floatAt(index, 1); - } - - public float positionY(int index) { - return floatAt(index, 2); - } - - public float positionZ(int index) { - return floatAt(index, 3); - } - - private float floatAt(int index, - int slot) { - checkIndex(index); - return floats[index * FLOAT_STRIDE + slot]; - } - - @Nonnull - private T object(int index, - int slot, - @Nonnull Class type) { - checkIndex(index); - return type.cast(objects[index * OBJECT_STRIDE + slot]); - } - - private void checkIndex(int index) { - if (index < 0 || index >= size) { - throw new IndexOutOfBoundsException(index); - } - } - - private void ensureCapacity(int required) { - int capacity = objects.length / OBJECT_STRIDE; - if (required <= capacity) { - return; - } - int nextCapacity = Math.max(required, capacity + (capacity >> 1)); - bodyKeyMostSignificantBits = Arrays.copyOf(bodyKeyMostSignificantBits, nextCapacity); - bodyKeyLeastSignificantBits = Arrays.copyOf(bodyKeyLeastSignificantBits, nextCapacity); - objects = Arrays.copyOf(objects, nextCapacity * OBJECT_STRIDE); - floats = Arrays.copyOf(floats, nextCapacity * FLOAT_STRIDE); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/batch/RigidBodySpawnTemplateBatch.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/batch/RigidBodySpawnTemplateBatch.java deleted file mode 100644 index d3856f9e..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/batch/RigidBodySpawnTemplateBatch.java +++ /dev/null @@ -1,224 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.batch; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import java.util.Arrays; -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Packed execution encoding for repeated rigid body spawns that share spawn properties. - */ -public final class RigidBodySpawnTemplateBatch { - - private static final int POSITION_STRIDE = 3; - - @Nonnull - private final SpaceId spaceId; - @Nonnull - private final PhysicsShapeSpec shape; - private final float mass; - @Nonnull - private final PhysicsBodyType bodyType; - @Nonnull - private final RigidBodySpawnSettings settings; - @Nonnull - private final PhysicsBodyKind kind; - @Nonnull - private final PhysicsBodyPersistenceMode persistenceMode; - private long[] bodyKeyMostSignificantBits; - private long[] bodyKeyLeastSignificantBits; - private float[] positions; - private int size; - - public RigidBodySpawnTemplateBatch(int expectedBodies, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - int capacity = Math.max(1, expectedBodies); - this.spaceId = Objects.requireNonNull(spaceId, "spaceId"); - this.shape = Objects.requireNonNull(shape, "shape"); - this.mass = mass; - this.bodyType = Objects.requireNonNull(bodyType, "bodyType"); - this.settings = Objects.requireNonNull(settings, "settings"); - this.kind = Objects.requireNonNull(kind, "kind"); - this.persistenceMode = Objects.requireNonNull(persistenceMode, "persistenceMode"); - bodyKeyMostSignificantBits = new long[capacity]; - bodyKeyLeastSignificantBits = new long[capacity]; - positions = new float[capacity * POSITION_STRIDE]; - } - - private RigidBodySpawnTemplateBatch(@Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode, - @Nonnull long[] bodyKeyMostSignificantBits, - @Nonnull long[] bodyKeyLeastSignificantBits, - @Nonnull float[] positions, - int size) { - this.spaceId = spaceId; - this.shape = shape; - this.mass = mass; - this.bodyType = bodyType; - this.settings = settings; - this.kind = kind; - this.persistenceMode = persistenceMode; - this.bodyKeyMostSignificantBits = bodyKeyMostSignificantBits; - this.bodyKeyLeastSignificantBits = bodyKeyLeastSignificantBits; - this.positions = positions; - this.size = size; - } - - public void add(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ) { - Objects.requireNonNull(bodyKey, "bodyKey"); - add(bodyKey.mostSignificantBits(), - bodyKey.leastSignificantBits(), - positionX, - positionY, - positionZ); - } - - public void add(long bodyKeyMostSignificantBits, - long bodyKeyLeastSignificantBits, - float positionX, - float positionY, - float positionZ) { - ensureCapacity(size + 1); - this.bodyKeyMostSignificantBits[size] = bodyKeyMostSignificantBits; - this.bodyKeyLeastSignificantBits[size] = bodyKeyLeastSignificantBits; - int positionOffset = size * POSITION_STRIDE; - positions[positionOffset] = positionX; - positions[positionOffset + 1] = positionY; - positions[positionOffset + 2] = positionZ; - size++; - } - - @Nonnull - public RigidBodySpawnTemplateBatch freeze() { - int positionSize = size * POSITION_STRIDE; - return new RigidBodySpawnTemplateBatch(spaceId, - shape, - mass, - bodyType, - settings, - kind, - persistenceMode, - this.bodyKeyMostSignificantBits.length == size - ? this.bodyKeyMostSignificantBits - : Arrays.copyOf(this.bodyKeyMostSignificantBits, size), - this.bodyKeyLeastSignificantBits.length == size - ? this.bodyKeyLeastSignificantBits - : Arrays.copyOf(this.bodyKeyLeastSignificantBits, size), - positions.length == positionSize ? positions : Arrays.copyOf(positions, positionSize), - size); - } - - public int size() { - return size; - } - - @Nonnull - public SpaceId spaceId() { - return spaceId; - } - - @Nonnull - public PhysicsShapeSpec shape() { - return shape; - } - - public float mass() { - return mass; - } - - @Nonnull - public PhysicsBodyType bodyType() { - return bodyType; - } - - @Nonnull - public RigidBodySpawnSettings settings() { - return settings; - } - - @Nonnull - public PhysicsBodyKind kind() { - return kind; - } - - @Nonnull - public PhysicsBodyPersistenceMode persistenceMode() { - return persistenceMode; - } - - @Nonnull - public RigidBodyKey bodyKey(int index) { - checkIndex(index); - return RigidBodyKey.of(bodyKeyMostSignificantBits[index], bodyKeyLeastSignificantBits[index]); - } - - @Nonnull - public long[] bodyKeyMostSignificantBits() { - return bodyKeyMostSignificantBits; - } - - @Nonnull - public long[] bodyKeyLeastSignificantBits() { - return bodyKeyLeastSignificantBits; - } - - @Nonnull - public float[] positions() { - return positions; - } - - public float positionX(int index) { - return positionAt(index, 0); - } - - public float positionY(int index) { - return positionAt(index, 1); - } - - public float positionZ(int index) { - return positionAt(index, 2); - } - - private float positionAt(int index, - int slot) { - checkIndex(index); - return positions[index * POSITION_STRIDE + slot]; - } - - private void ensureCapacity(int required) { - if (required <= bodyKeyMostSignificantBits.length) { - return; - } - int nextCapacity = Math.max(required, - bodyKeyMostSignificantBits.length + (bodyKeyMostSignificantBits.length >> 1)); - bodyKeyMostSignificantBits = Arrays.copyOf(bodyKeyMostSignificantBits, nextCapacity); - bodyKeyLeastSignificantBits = Arrays.copyOf(bodyKeyLeastSignificantBits, nextCapacity); - positions = Arrays.copyOf(positions, nextCapacity * POSITION_STRIDE); - } - - private void checkIndex(int index) { - if (index < 0 || index >= size) { - throw new IndexOutOfBoundsException(index); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/BenchmarkSpaceStatsQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/BenchmarkSpaceStatsQuery.java deleted file mode 100644 index c4062832..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/BenchmarkSpaceStatsQuery.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.query; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.simulation.view.BenchmarkSpaceStatsView; - -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Internal owner-lane query for stress benchmark health counters. - */ -public record BenchmarkSpaceStatsQuery(@Nonnull SpaceId spaceId, - float groundY, - float belowPlaneTolerance, - float bodyWorldMinY, - float bodyVoidY, - boolean includeTerrainProbe) - implements PhysicsInternalQuery { - - public BenchmarkSpaceStatsQuery { - Objects.requireNonNull(spaceId, "spaceId"); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/PhysicsDebugContactsQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/PhysicsDebugContactsQuery.java deleted file mode 100644 index 03cc1839..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/PhysicsDebugContactsQuery.java +++ /dev/null @@ -1,24 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.query; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsDebugContactView; - -import java.util.List; -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Internal owner-lane query for nearby contact points used by debug rendering. - */ -public record PhysicsDebugContactsQuery(@Nonnull SpaceId spaceId, - double viewerX, - double viewerY, - double viewerZ, - double viewRadius, - int maxContacts) - implements PhysicsInternalQuery> { - - public PhysicsDebugContactsQuery { - Objects.requireNonNull(spaceId, "spaceId"); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/PhysicsDebugJointsQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/PhysicsDebugJointsQuery.java deleted file mode 100644 index 8a6ae246..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/PhysicsDebugJointsQuery.java +++ /dev/null @@ -1,24 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.query; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsDebugJointView; - -import java.util.List; -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Internal owner-lane query for nearby joints used by debug rendering. - */ -public record PhysicsDebugJointsQuery(@Nonnull SpaceId spaceId, - double viewerX, - double viewerY, - double viewerZ, - double viewRadius, - int maxJoints) - implements PhysicsInternalQuery> { - - public PhysicsDebugJointsQuery { - Objects.requireNonNull(spaceId, "spaceId"); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/PhysicsInternalQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/PhysicsInternalQuery.java deleted file mode 100644 index 444eb945..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/PhysicsInternalQuery.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.query; - -/** - * Internal owner-lane read that returns copied physics data without widening plugin ABI. - * - * @param immutable query result type - */ -public sealed interface PhysicsInternalQuery permits BenchmarkSpaceStatsQuery, - PhysicsDebugContactsQuery, - PhysicsDebugJointsQuery, - PhysicsSpaceRuntimeStatsQuery, - WorldCollisionPrewarmEnvelopeQuery { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/PhysicsSpaceRuntimeStatsQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/PhysicsSpaceRuntimeStatsQuery.java deleted file mode 100644 index cd7b6b2a..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/PhysicsSpaceRuntimeStatsQuery.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.query; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsSpaceRuntimeStatsView; - -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Internal owner-lane query for live backend and registry counters in one physics space. - */ -public record PhysicsSpaceRuntimeStatsQuery(@Nonnull SpaceId spaceId) - implements PhysicsInternalQuery { - - public PhysicsSpaceRuntimeStatsQuery { - Objects.requireNonNull(spaceId, "spaceId"); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/WorldCollisionPrewarmEnvelopeQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/WorldCollisionPrewarmEnvelopeQuery.java deleted file mode 100644 index 8a50f599..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/query/WorldCollisionPrewarmEnvelopeQuery.java +++ /dev/null @@ -1,42 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.query; - -import com.hypixel.hytale.server.core.universe.world.World; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionPrewarmStats; -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Internal owner-lane query that computes collision prewarm coverage for a spawn envelope. - */ -public record WorldCollisionPrewarmEnvelopeQuery(@Nonnull World world, - @Nonnull SpaceId spaceId, - int count, - float originX, - float originY, - float originZ, - int side, - float spacing, - int radius, - float fallEnvelopeMinY, - float horizontalDriftHaloBlocks, - long tick) - implements PhysicsInternalQuery { - - public WorldCollisionPrewarmEnvelopeQuery { - Objects.requireNonNull(world, "world"); - Objects.requireNonNull(spaceId, "spaceId"); - if (count < 0) { - throw new IllegalArgumentException("count must be non-negative"); - } - if (side <= 0) { - throw new IllegalArgumentException("side must be positive"); - } - if (!Float.isFinite(spacing) || spacing <= 0.0f) { - throw new IllegalArgumentException("spacing must be finite and positive"); - } - if (radius < 0) { - throw new IllegalArgumentException("radius must be non-negative"); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutableJointCommandRecorder.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutableJointCommandRecorder.java deleted file mode 100644 index a56f3d7f..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutableJointCommandRecorder.java +++ /dev/null @@ -1,352 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.recorder; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.JointCommandRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.JointType; -import java.util.Objects; -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -/** - * Fluent recorder for one joint creation command. - */ -public final class MutableJointCommandRecorder implements JointCommandRecorder { - - @Nonnull - private final MutablePhysicsCommandContext recorder; - @Nonnull - private final JointKey jointKey; - private SpaceId spaceId; - private RigidBodyKey bodyA; - private RigidBodyKey bodyB; - private JointType type; - private float anchorAX; - private float anchorAY; - private float anchorAZ; - private float anchorBX; - private float anchorBY; - private float anchorBZ; - private float axisX; - private float axisY; - private float axisZ; - private float restLength; - private float stiffness; - private float damping; - private float lowerLimit; - private float upperLimit; - private boolean motorEnabled; - private float motorTargetVelocity; - private float motorMaxForce; - private boolean sealed; - - MutableJointCommandRecorder(@Nonnull MutablePhysicsCommandContext recorder, - @Nonnull JointKey jointKey) { - this.recorder = Objects.requireNonNull(recorder, "recorder"); - this.jointKey = Objects.requireNonNull(jointKey, "jointKey"); - } - - @Nonnull - @Override - public JointCommandRecorder space(@Nonnull SpaceId spaceId) { - assertOpen(); - this.spaceId = Objects.requireNonNull(spaceId, "spaceId"); - return this; - } - - @Nonnull - @Override - public JointCommandRecorder bodies(@Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB) { - assertOpen(); - this.bodyA = Objects.requireNonNull(bodyA, "bodyA"); - this.bodyB = Objects.requireNonNull(bodyB, "bodyB"); - return this; - } - - @Nonnull - @Override - public JointCommandRecorder fixed(@Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB) { - Objects.requireNonNull(anchorA, "anchorA"); - Objects.requireNonNull(anchorB, "anchorB"); - return fixed(anchorA.x, anchorA.y, anchorA.z, anchorB.x, anchorB.y, anchorB.z); - } - - @Nonnull - @Override - public JointCommandRecorder fixed(float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ) { - return anchors(JointType.FIXED, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - 0.0f, - 0.0f, - 0.0f); - } - - @Nonnull - @Override - public JointCommandRecorder point(@Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB) { - Objects.requireNonNull(anchorA, "anchorA"); - Objects.requireNonNull(anchorB, "anchorB"); - return point(anchorA.x, anchorA.y, anchorA.z, anchorB.x, anchorB.y, anchorB.z); - } - - @Nonnull - @Override - public JointCommandRecorder point(float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ) { - return anchors(JointType.POINT, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - 0.0f, - 0.0f, - 0.0f); - } - - @Nonnull - @Override - public JointCommandRecorder hinge(@Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis) { - Objects.requireNonNull(anchorA, "anchorA"); - Objects.requireNonNull(anchorB, "anchorB"); - Objects.requireNonNull(axis, "axis"); - return hinge(anchorA.x, - anchorA.y, - anchorA.z, - anchorB.x, - anchorB.y, - anchorB.z, - axis.x, - axis.y, - axis.z); - } - - @Nonnull - @Override - public JointCommandRecorder hinge(float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ) { - return anchors(JointType.HINGE, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - axisX, - axisY, - axisZ); - } - - @Nonnull - @Override - public JointCommandRecorder slider(@Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis) { - Objects.requireNonNull(anchorA, "anchorA"); - Objects.requireNonNull(anchorB, "anchorB"); - Objects.requireNonNull(axis, "axis"); - return slider(anchorA.x, - anchorA.y, - anchorA.z, - anchorB.x, - anchorB.y, - anchorB.z, - axis.x, - axis.y, - axis.z); - } - - @Nonnull - @Override - public JointCommandRecorder slider(float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ) { - return anchors(JointType.SLIDER, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - axisX, - axisY, - axisZ); - } - - @Nonnull - @Override - public JointCommandRecorder spring(@Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - float restLength, - float stiffness, - float damping) { - Objects.requireNonNull(anchorA, "anchorA"); - Objects.requireNonNull(anchorB, "anchorB"); - spring(anchorA.x, - anchorA.y, - anchorA.z, - anchorB.x, - anchorB.y, - anchorB.z, - restLength, - stiffness, - damping); - return this; - } - - @Nonnull - @Override - public JointCommandRecorder spring(float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float restLength, - float stiffness, - float damping) { - anchors(JointType.SPRING, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - 0.0f, - 0.0f, - 0.0f); - this.restLength = restLength; - this.stiffness = stiffness; - this.damping = damping; - return this; - } - - @Nonnull - @Override - public JointCommandRecorder limits(float lowerLimit, - float upperLimit) { - assertOpen(); - this.lowerLimit = lowerLimit; - this.upperLimit = upperLimit; - return this; - } - - @Nonnull - @Override - public JointCommandRecorder motor(float targetVelocity, - float maxForce) { - assertOpen(); - this.motorEnabled = true; - this.motorTargetVelocity = targetVelocity; - this.motorMaxForce = maxForce; - return this; - } - - void record() { - assertOpen(); - validate(); - recorder.recordJoint(jointKey, - spaceId, - bodyA, - bodyB, - type, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - axisX, - axisY, - axisZ, - restLength, - stiffness, - damping, - lowerLimit, - upperLimit, - motorEnabled, - motorTargetVelocity, - motorMaxForce); - } - - void validate() { - if (spaceId == null) { - throw new IllegalStateException("Joint command requires a physics space"); - } - if (bodyA == null || bodyB == null) { - throw new IllegalStateException("Joint command requires both rigid bodies"); - } - if (type == null) { - throw new IllegalStateException("Joint command requires a joint type"); - } - } - - @Nonnull - private JointCommandRecorder anchors(@Nonnull JointType type, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ) { - assertOpen(); - this.type = type; - this.anchorAX = anchorAX; - this.anchorAY = anchorAY; - this.anchorAZ = anchorAZ; - this.anchorBX = anchorBX; - this.anchorBY = anchorBY; - this.anchorBZ = anchorBZ; - this.axisX = axisX; - this.axisY = axisY; - this.axisZ = axisZ; - return this; - } - - void seal() { - sealed = true; - } - - private void assertOpen() { - if (sealed) { - throw new IllegalStateException("Joint command recorder is no longer active"); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutablePhysicsCommandContext.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutablePhysicsCommandContext.java deleted file mode 100644 index 21492c60..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutablePhysicsCommandContext.java +++ /dev/null @@ -1,588 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.recorder; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.simulation.PhysicsCommandOperations; -import dev.hytalemodding.impulse.core.internal.simulation.batch.RecordedPhysicsCommandBatch; -import dev.hytalemodding.impulse.core.internal.simulation.batch.RigidBodySpawnBatch; -import dev.hytalemodding.impulse.core.internal.simulation.batch.RigidBodySpawnTemplateBatch; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.JointCommandRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.JointType; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandBatch; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandContext; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandMetadata; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandRecipe; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.RigidBodyCommandRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.RigidBodySpawnBatchRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.RigidBodySpawnRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.RigidBodySpawnTemplateRecorder; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.function.Consumer; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Mutable recorder for physics simulation commands. - * - *

Mutable contexts are short-lived. Record commands during an ECS/server tick, then - * submit the context to freeze it into an immutable {@link PhysicsCommandBatch}.

- */ -public final class MutablePhysicsCommandContext implements PhysicsCommandContext { - - private static final int DEFAULT_EXPECTED_OPERATIONS = 8; - - private final long submittedServerTick; - private final long worldEpoch; - private final PhysicsCommandOperations operations; - private final List pendingSpawns = new ArrayList<>(); - private final List pendingJoints = new ArrayList<>(); - private boolean frozen; - - public MutablePhysicsCommandContext(long submittedServerTick, long worldEpoch) { - this(submittedServerTick, worldEpoch, DEFAULT_EXPECTED_OPERATIONS); - } - - public MutablePhysicsCommandContext(long submittedServerTick, - long worldEpoch, - int expectedOperations) { - this.submittedServerTick = Math.max(0L, submittedServerTick); - this.worldEpoch = Math.max(0L, worldEpoch); - this.operations = new PhysicsCommandOperations(expectedOperations); - } - - @Nonnull - @Override - public PhysicsCommandContext compose(@Nonnull PhysicsCommandRecipe recipe) { - Objects.requireNonNull(recipe, "recipe").record(this); - return this; - } - - @Nonnull - @Override - public RigidBodyCommandRecorder body(@Nonnull RigidBodyKey bodyKey) { - return new MutableRigidBodyCommandRecorder(this, bodyKey); - } - - @Nonnull - @Override - public PhysicsCommandContext body(@Nonnull RigidBodyKey bodyKey, - @Nonnull Consumer recipe) { - MutableRigidBodyCommandRecorder body = new MutableRigidBodyCommandRecorder(this, bodyKey); - try { - Objects.requireNonNull(recipe, "recipe").accept(body); - } finally { - body.seal(); - } - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext setSpaceGravity(@Nonnull SpaceId spaceId, - float x, - float y, - float z) { - recordSetSpaceGravity(spaceId, x, y, z); - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext setBodyTransform(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ, - float rotationX, - float rotationY, - float rotationZ, - float rotationW, - boolean activate) { - recordSetTransform(bodyKey, - positionX, - positionY, - positionZ, - rotationX, - rotationY, - rotationZ, - rotationW, - activate); - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext setBodyVelocity(@Nonnull RigidBodyKey bodyKey, - float linearX, - float linearY, - float linearZ, - float angularX, - float angularY, - float angularZ, - boolean activate) { - recordSetVelocity(bodyKey, linearX, linearY, linearZ, angularX, angularY, angularZ, activate); - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext setBodyPosition(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ, - boolean activate) { - recordSetPosition(bodyKey, positionX, positionY, positionZ, activate); - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext setBodyType(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodyType bodyType, - boolean activate) { - recordSetType(bodyKey, bodyType, activate); - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext activateBody(@Nonnull RigidBodyKey bodyKey) { - recordActivate(bodyKey); - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext applyBodyImpulse(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z) { - recordImpulse(bodyKey, x, y, z, false, 0.0f, 0.0f, 0.0f, false); - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext applyBodyImpulse(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ) { - recordImpulse(bodyKey, x, y, z, true, offsetX, offsetY, offsetZ, false); - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext applyBodyTorqueImpulse(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z) { - recordImpulse(bodyKey, x, y, z, false, 0.0f, 0.0f, 0.0f, true); - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext applyBodyForce(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z) { - recordForce(bodyKey, x, y, z, false, 0.0f, 0.0f, 0.0f, false); - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext applyBodyForce(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ) { - recordForce(bodyKey, x, y, z, true, offsetX, offsetY, offsetZ, false); - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext applyBodyTorque(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z) { - recordForce(bodyKey, x, y, z, false, 0.0f, 0.0f, 0.0f, true); - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext destroyBody(@Nonnull RigidBodyKey bodyKey) { - recordDestroyBody(bodyKey); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder spawnBody(@Nonnull RigidBodyKey bodyKey) { - assertMutable(); - MutableRigidBodySpawnRecorder spawn = new MutableRigidBodySpawnRecorder(this::recordSpawn, bodyKey); - pendingSpawns.add(spawn); - return spawn; - } - - @Nonnull - @Override - public PhysicsCommandContext spawnBody(@Nonnull RigidBodyKey bodyKey, - @Nonnull Consumer recipe) { - MutableRigidBodySpawnRecorder spawn = - new MutableRigidBodySpawnRecorder(this::recordSpawn, bodyKey); - try { - Objects.requireNonNull(recipe, "recipe").accept(spawn); - spawn.record(); - } finally { - spawn.seal(); - } - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext spawnBodies( - @Nonnull Consumer recipe) { - return spawnBodies(DEFAULT_EXPECTED_OPERATIONS, recipe); - } - - @Nonnull - @Override - public PhysicsCommandContext spawnBodies(int expectedBodies, - @Nonnull Consumer recipe) { - MutableRigidBodySpawnBatchRecorder spawns = - new MutableRigidBodySpawnBatchRecorder(expectedBodies); - try { - Objects.requireNonNull(recipe, "recipe").accept(spawns); - } finally { - spawns.seal(); - } - if (!spawns.isEmpty()) { - recordSpawnBatch(spawns.spawns()); - } - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext spawnBodies(int expectedBodies, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode, - @Nonnull Consumer recipe) { - MutableRigidBodySpawnTemplateRecorder spawns = new MutableRigidBodySpawnTemplateRecorder(expectedBodies, - spaceId, - shape, - mass, - bodyType, - settings, - kind, - persistenceMode); - try { - Objects.requireNonNull(recipe, "recipe").accept(spawns); - } finally { - spawns.seal(); - } - if (!spawns.isEmpty()) { - recordSpawnTemplateBatch(spawns.spawns()); - } - return this; - } - - @Nonnull - @Override - public JointCommandRecorder joint(@Nonnull JointKey jointKey) { - assertMutable(); - MutableJointCommandRecorder joint = new MutableJointCommandRecorder(this, jointKey); - pendingJoints.add(joint); - return joint; - } - - @Nonnull - @Override - public PhysicsCommandContext joint(@Nonnull JointKey jointKey, - @Nonnull Consumer recipe) { - MutableJointCommandRecorder joint = new MutableJointCommandRecorder(this, jointKey); - try { - Objects.requireNonNull(recipe, "recipe").accept(joint); - joint.record(); - } finally { - joint.seal(); - } - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext destroyJoint(@Nonnull JointKey jointKey) { - recordDestroyJoint(jointKey); - return this; - } - - @Nonnull - @Override - public PhysicsCommandContext destroyJointBetween(@Nullable JointKey preferredJointKey, - @Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB) { - recordDestroyJointBetween(preferredJointKey, spaceId, bodyA, bodyB); - return this; - } - - @Nonnull - public RecordedPhysicsCommandBatch freezeInternal(long commandBatchSequence) { - if (frozen) { - throw new IllegalStateException("Physics command context is already frozen"); - } - recordPendingRecorders(); - frozen = true; - return new RecordedPhysicsCommandBatch( - new PhysicsCommandMetadata(submittedServerTick, commandBatchSequence), - worldEpoch, - operations.freeze()); - } - - public boolean isEmpty() { - return operations.size() == 0 && pendingSpawns.isEmpty() && pendingJoints.isEmpty(); - } - - private void recordPendingRecorders() { - try { - for (MutableRigidBodySpawnRecorder spawn : pendingSpawns) { - spawn.validate(); - } - for (MutableJointCommandRecorder joint : pendingJoints) { - joint.validate(); - } - for (MutableRigidBodySpawnRecorder spawn : pendingSpawns) { - spawn.record(); - } - for (MutableJointCommandRecorder joint : pendingJoints) { - joint.record(); - } - } finally { - for (MutableRigidBodySpawnRecorder spawn : pendingSpawns) { - spawn.seal(); - } - for (MutableJointCommandRecorder joint : pendingJoints) { - joint.seal(); - } - pendingSpawns.clear(); - pendingJoints.clear(); - } - } - - void recordSpawn(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - float positionX, - float positionY, - float positionZ, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - assertMutable(); - operations.addSpawn(bodyKey, - spaceId, - shape, - mass, - bodyType, - positionX, - positionY, - positionZ, - settings, - kind, - persistenceMode); - } - - void recordSpawnBatch(@Nonnull RigidBodySpawnBatch spawns) { - assertMutable(); - operations.addSpawnBatch(spawns); - } - - void recordSpawnTemplateBatch(@Nonnull RigidBodySpawnTemplateBatch spawns) { - assertMutable(); - operations.addSpawnTemplateBatch(spawns); - } - - void recordDestroyBody(@Nonnull RigidBodyKey bodyKey) { - assertMutable(); - operations.addDestroyBody(bodyKey); - } - - void recordSetSpaceGravity(@Nonnull SpaceId spaceId, - float x, - float y, - float z) { - assertMutable(); - operations.addSetSpaceGravity(spaceId, x, y, z); - } - - void recordSetTransform(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ, - float rotationX, - float rotationY, - float rotationZ, - float rotationW, - boolean activate) { - assertMutable(); - operations.addSetTransform(bodyKey, - positionX, - positionY, - positionZ, - rotationX, - rotationY, - rotationZ, - rotationW, - activate); - } - - void recordSetVelocity(@Nonnull RigidBodyKey bodyKey, - float linearX, - float linearY, - float linearZ, - float angularX, - float angularY, - float angularZ, - boolean activate) { - assertMutable(); - operations.addSetVelocity(bodyKey, linearX, linearY, linearZ, angularX, angularY, angularZ, activate); - } - - void recordSetPosition(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ, - boolean activate) { - assertMutable(); - operations.addSetPosition(bodyKey, positionX, positionY, positionZ, activate); - } - - void recordSetType(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodyType bodyType, - boolean activate) { - assertMutable(); - operations.addSetType(bodyKey, bodyType, activate); - } - - void recordActivate(@Nonnull RigidBodyKey bodyKey) { - assertMutable(); - operations.addActivate(bodyKey); - } - - void recordImpulse(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z, - boolean hasOffset, - float offsetX, - float offsetY, - float offsetZ, - boolean torque) { - assertMutable(); - operations.addImpulse(bodyKey, x, y, z, hasOffset, offsetX, offsetY, offsetZ, torque); - } - - void recordForce(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z, - boolean hasOffset, - float offsetX, - float offsetY, - float offsetZ, - boolean torque) { - assertMutable(); - operations.addForce(bodyKey, x, y, z, hasOffset, offsetX, offsetY, offsetZ, torque); - } - - void recordJoint(@Nonnull JointKey jointKey, - @Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB, - @Nonnull JointType type, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ, - float restLength, - float stiffness, - float damping, - float lowerLimit, - float upperLimit, - boolean motorEnabled, - float motorTargetVelocity, - float motorMaxForce) { - assertMutable(); - operations.addJoint(jointKey, - spaceId, - bodyA, - bodyB, - type, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - axisX, - axisY, - axisZ, - restLength, - stiffness, - damping, - lowerLimit, - upperLimit, - motorEnabled, - motorTargetVelocity, - motorMaxForce); - } - - void recordDestroyJoint(@Nonnull JointKey jointKey) { - assertMutable(); - operations.addDestroyJoint(jointKey); - } - - void recordDestroyJointBetween(@Nullable JointKey preferredJointKey, - @Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB) { - assertMutable(); - operations.addDestroyJointBetween(preferredJointKey, spaceId, bodyA, bodyB); - } - - private void assertMutable() { - if (frozen) { - throw new IllegalStateException("Physics command context is already frozen"); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutableRigidBodyCommandRecorder.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutableRigidBodyCommandRecorder.java deleted file mode 100644 index 94692876..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutableRigidBodyCommandRecorder.java +++ /dev/null @@ -1,296 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.recorder; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.RigidBodyCommandRecorder; -import java.util.Objects; -import javax.annotation.Nonnull; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -/** - * Fluent recorder for operations targeting one rigid body key. - */ -public final class MutableRigidBodyCommandRecorder implements RigidBodyCommandRecorder { - - @Nonnull - private final MutablePhysicsCommandContext recorder; - @Nonnull - private final RigidBodyKey bodyKey; - private boolean sealed; - - MutableRigidBodyCommandRecorder(@Nonnull MutablePhysicsCommandContext recorder, - @Nonnull RigidBodyKey bodyKey) { - this.recorder = Objects.requireNonNull(recorder, "recorder"); - this.bodyKey = Objects.requireNonNull(bodyKey, "bodyKey"); - } - - @Nonnull - @Override - public RigidBodyCommandRecorder setTransform(@Nonnull Vector3f position, - @Nonnull Quaternionf rotation) { - return setTransform(position, rotation, false); - } - - @Nonnull - @Override - public RigidBodyCommandRecorder setTransform(@Nonnull Vector3f position, - @Nonnull Quaternionf rotation, - boolean activate) { - Objects.requireNonNull(position, "position"); - Objects.requireNonNull(rotation, "rotation"); - return setTransform(position.x, - position.y, - position.z, - rotation.x, - rotation.y, - rotation.z, - rotation.w, - activate); - } - - @Nonnull - @Override - public RigidBodyCommandRecorder setTransform(float positionX, - float positionY, - float positionZ, - float rotationX, - float rotationY, - float rotationZ, - float rotationW, - boolean activate) { - assertOpen(); - recorder.recordSetTransform(bodyKey, - positionX, - positionY, - positionZ, - rotationX, - rotationY, - rotationZ, - rotationW, - activate); - return this; - } - - @Nonnull - @Override - public RigidBodyCommandRecorder setPosition(@Nonnull Vector3f position) { - return setPosition(position, false); - } - - @Nonnull - @Override - public RigidBodyCommandRecorder setPosition(@Nonnull Vector3f position, - boolean activate) { - Objects.requireNonNull(position, "position"); - return setPosition(position.x, position.y, position.z, activate); - } - - @Nonnull - @Override - public RigidBodyCommandRecorder setPosition(float positionX, - float positionY, - float positionZ, - boolean activate) { - assertOpen(); - recorder.recordSetPosition(bodyKey, positionX, positionY, positionZ, activate); - return this; - } - - @Nonnull - @Override - public RigidBodyCommandRecorder setVelocity(@Nonnull Vector3f linearVelocity, - @Nonnull Vector3f angularVelocity) { - return setVelocity(linearVelocity, angularVelocity, false); - } - - @Nonnull - @Override - public RigidBodyCommandRecorder setVelocity(@Nonnull Vector3f linearVelocity, - @Nonnull Vector3f angularVelocity, - boolean activate) { - Objects.requireNonNull(linearVelocity, "linearVelocity"); - Objects.requireNonNull(angularVelocity, "angularVelocity"); - return setVelocity(linearVelocity.x, - linearVelocity.y, - linearVelocity.z, - angularVelocity.x, - angularVelocity.y, - angularVelocity.z, - activate); - } - - @Nonnull - @Override - public RigidBodyCommandRecorder setVelocity(float linearX, - float linearY, - float linearZ, - float angularX, - float angularY, - float angularZ, - boolean activate) { - assertOpen(); - recorder.recordSetVelocity(bodyKey, - linearX, - linearY, - linearZ, - angularX, - angularY, - angularZ, - activate); - return this; - } - - @Nonnull - @Override - public RigidBodyCommandRecorder setType(@Nonnull PhysicsBodyType bodyType) { - return setType(bodyType, false); - } - - @Nonnull - @Override - public RigidBodyCommandRecorder setType(@Nonnull PhysicsBodyType bodyType, - boolean activate) { - assertOpen(); - recorder.recordSetType(bodyKey, bodyType, activate); - return this; - } - - @Nonnull - @Override - public RigidBodyCommandRecorder activate() { - assertOpen(); - recorder.recordActivate(bodyKey); - return this; - } - - @Nonnull - @Override - public RigidBodyCommandRecorder applyImpulse(@Nonnull Vector3f impulse) { - Objects.requireNonNull(impulse, "impulse"); - return applyImpulse(impulse.x, impulse.y, impulse.z); - } - - @Nonnull - @Override - public RigidBodyCommandRecorder applyImpulse(float x, - float y, - float z) { - assertOpen(); - recorder.recordImpulse(bodyKey, x, y, z, false, 0.0f, 0.0f, 0.0f, false); - return this; - } - - @Nonnull - @Override - public RigidBodyCommandRecorder applyImpulse(@Nonnull Vector3f impulse, - @Nonnull Vector3f offset) { - Objects.requireNonNull(impulse, "impulse"); - Objects.requireNonNull(offset, "offset"); - return applyImpulse(impulse.x, impulse.y, impulse.z, offset.x, offset.y, offset.z); - } - - @Nonnull - @Override - public RigidBodyCommandRecorder applyImpulse(float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ) { - assertOpen(); - recorder.recordImpulse(bodyKey, x, y, z, true, offsetX, offsetY, offsetZ, false); - return this; - } - - @Nonnull - @Override - public RigidBodyCommandRecorder applyTorqueImpulse(@Nonnull Vector3f impulse) { - Objects.requireNonNull(impulse, "impulse"); - return applyTorqueImpulse(impulse.x, impulse.y, impulse.z); - } - - @Nonnull - @Override - public RigidBodyCommandRecorder applyTorqueImpulse(float x, - float y, - float z) { - assertOpen(); - recorder.recordImpulse(bodyKey, x, y, z, false, 0.0f, 0.0f, 0.0f, true); - return this; - } - - @Nonnull - @Override - public RigidBodyCommandRecorder applyForce(@Nonnull Vector3f force) { - Objects.requireNonNull(force, "force"); - return applyForce(force.x, force.y, force.z); - } - - @Nonnull - @Override - public RigidBodyCommandRecorder applyForce(float x, - float y, - float z) { - assertOpen(); - recorder.recordForce(bodyKey, x, y, z, false, 0.0f, 0.0f, 0.0f, false); - return this; - } - - @Nonnull - @Override - public RigidBodyCommandRecorder applyForce(@Nonnull Vector3f force, - @Nonnull Vector3f offset) { - Objects.requireNonNull(force, "force"); - Objects.requireNonNull(offset, "offset"); - return applyForce(force.x, force.y, force.z, offset.x, offset.y, offset.z); - } - - @Nonnull - @Override - public RigidBodyCommandRecorder applyForce(float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ) { - assertOpen(); - recorder.recordForce(bodyKey, x, y, z, true, offsetX, offsetY, offsetZ, false); - return this; - } - - @Nonnull - @Override - public RigidBodyCommandRecorder applyTorque(@Nonnull Vector3f force) { - Objects.requireNonNull(force, "force"); - return applyTorque(force.x, force.y, force.z); - } - - @Nonnull - @Override - public RigidBodyCommandRecorder applyTorque(float x, - float y, - float z) { - assertOpen(); - recorder.recordForce(bodyKey, x, y, z, false, 0.0f, 0.0f, 0.0f, true); - return this; - } - - @Nonnull - @Override - public RigidBodyCommandRecorder destroy() { - assertOpen(); - recorder.recordDestroyBody(bodyKey); - return this; - } - - void seal() { - sealed = true; - } - - private void assertOpen() { - if (sealed) { - throw new IllegalStateException("Rigid body command recorder is no longer active"); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutableRigidBodySpawnBatchRecorder.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutableRigidBodySpawnBatchRecorder.java deleted file mode 100644 index 4f7b5911..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutableRigidBodySpawnBatchRecorder.java +++ /dev/null @@ -1,122 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.recorder; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.simulation.batch.RigidBodySpawnBatch; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.RigidBodySpawnBatchRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.RigidBodySpawnRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import java.util.Objects; -import java.util.function.Consumer; -import javax.annotation.Nonnull; - -/** - * Fluent recorder for a bulk rigid body spawn command. - */ -public final class MutableRigidBodySpawnBatchRecorder implements RigidBodySpawnBatchRecorder { - - @Nonnull - private final RigidBodySpawnBatch spawns; - private boolean sealed; - - MutableRigidBodySpawnBatchRecorder(int expectedBodies) { - spawns = new RigidBodySpawnBatch(expectedBodies); - } - - @Nonnull - @Override - public RigidBodySpawnBatchRecorder body(@Nonnull RigidBodyKey bodyKey, - @Nonnull Consumer recipe) { - assertMutable(); - MutableRigidBodySpawnRecorder spawn = new MutableRigidBodySpawnRecorder(spawns::add, bodyKey); - try { - Objects.requireNonNull(recipe, "recipe").accept(spawn); - spawn.record(); - } finally { - spawn.seal(); - } - return this; - } - - @Nonnull - @Override - public RigidBodySpawnBatchRecorder body(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - float positionX, - float positionY, - float positionZ, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - assertMutable(); - Objects.requireNonNull(bodyKey, "bodyKey"); - return body(bodyKey.mostSignificantBits(), - bodyKey.leastSignificantBits(), - spaceId, - shape, - mass, - bodyType, - positionX, - positionY, - positionZ, - settings, - kind, - persistenceMode); - } - - @Nonnull - @Override - public RigidBodySpawnBatchRecorder body(long bodyKeyMostSignificantBits, - long bodyKeyLeastSignificantBits, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - float positionX, - float positionY, - float positionZ, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - assertMutable(); - spawns.add(bodyKeyMostSignificantBits, - bodyKeyLeastSignificantBits, - spaceId, - shape, - mass, - bodyType, - positionX, - positionY, - positionZ, - settings, - kind, - persistenceMode); - return this; - } - - boolean isEmpty() { - return spawns.size() == 0; - } - - @Nonnull - RigidBodySpawnBatch spawns() { - return spawns; - } - - void seal() { - sealed = true; - } - - private void assertMutable() { - if (sealed) { - throw new IllegalStateException("Rigid body spawn batch is already recorded"); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutableRigidBodySpawnRecorder.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutableRigidBodySpawnRecorder.java deleted file mode 100644 index 96d352f6..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutableRigidBodySpawnRecorder.java +++ /dev/null @@ -1,263 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.recorder; - -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.RigidBodySpawnRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import java.util.Objects; -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -/** - * Fluent recorder for one copied rigid body spawn request. - */ -public final class MutableRigidBodySpawnRecorder implements RigidBodySpawnRecorder { - - @Nonnull - private final RigidBodySpawnSink sink; - @Nonnull - private final RigidBodyKey bodyKey; - private SpaceId spaceId; - private PhysicsShapeSpec shape; - private float mass = 1.0f; - @Nonnull - private PhysicsBodyType bodyType = PhysicsBodyType.DYNAMIC; - private float positionX; - private float positionY; - private float positionZ; - private RigidBodySpawnSettings settings; - @Nonnull - private PhysicsBodyKind kind = PhysicsBodyKind.BODY; - @Nonnull - private PhysicsBodyPersistenceMode persistenceMode = PhysicsBodyPersistenceMode.RUNTIME_ONLY; - private boolean sealed; - - MutableRigidBodySpawnRecorder(@Nonnull RigidBodySpawnSink sink, - @Nonnull RigidBodyKey bodyKey) { - this.sink = Objects.requireNonNull(sink, "sink"); - this.bodyKey = Objects.requireNonNull(bodyKey, "bodyKey"); - } - - @Nonnull - @Override - public RigidBodySpawnRecorder space(@Nonnull SpaceId spaceId) { - assertMutable(); - this.spaceId = Objects.requireNonNull(spaceId, "spaceId"); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder box(float halfX, - float halfY, - float halfZ) { - assertMutable(); - this.shape = PhysicsShapeSpec.box(halfX, halfY, halfZ); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder shape(@Nonnull PhysicsShapeSpec shape) { - assertMutable(); - this.shape = Objects.requireNonNull(shape, "shape"); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder sphere(float radius) { - assertMutable(); - this.shape = PhysicsShapeSpec.sphere(radius); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder capsule(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis) { - assertMutable(); - this.shape = PhysicsShapeSpec.capsule(radius, halfHeight, axis); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder cylinder(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis) { - assertMutable(); - this.shape = PhysicsShapeSpec.cylinder(radius, halfHeight, axis); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder cone(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis) { - assertMutable(); - this.shape = PhysicsShapeSpec.cone(radius, halfHeight, axis); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder plane(float groundY) { - assertMutable(); - this.shape = PhysicsShapeSpec.plane(groundY); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder mass(float mass) { - assertMutable(); - this.mass = mass; - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder type(@Nonnull PhysicsBodyType bodyType) { - assertMutable(); - this.bodyType = Objects.requireNonNull(bodyType, "bodyType"); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder dynamic() { - return type(PhysicsBodyType.DYNAMIC); - } - - @Nonnull - @Override - public RigidBodySpawnRecorder kinematic() { - return type(PhysicsBodyType.KINEMATIC); - } - - @Nonnull - @Override - public RigidBodySpawnRecorder position(@Nonnull Vector3f position) { - Objects.requireNonNull(position, "position"); - return position(position.x, position.y, position.z); - } - - @Nonnull - @Override - public RigidBodySpawnRecorder position(float x, - float y, - float z) { - assertMutable(); - this.positionX = x; - this.positionY = y; - this.positionZ = z; - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder settings(@Nonnull RigidBodySpawnSettings settings) { - assertMutable(); - this.settings = Objects.requireNonNull(settings, "settings"); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder sensor(boolean sensor) { - assertMutable(); - this.settings = settingsOrDefaults().withSensor(sensor); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder collisionFilter(int group, - int mask) { - assertMutable(); - this.settings = settingsOrDefaults().withCollisionFilter(group, mask); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder kind(@Nonnull PhysicsBodyKind kind) { - assertMutable(); - this.kind = Objects.requireNonNull(kind, "kind"); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder temporary() { - return kind(PhysicsBodyKind.TEMPORARY); - } - - @Nonnull - @Override - public RigidBodySpawnRecorder persistence(@Nonnull PhysicsBodyPersistenceMode persistenceMode) { - assertMutable(); - this.persistenceMode = Objects.requireNonNull(persistenceMode, "persistenceMode"); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnRecorder runtimeOnly() { - return persistence(PhysicsBodyPersistenceMode.RUNTIME_ONLY); - } - - @Nonnull - @Override - public RigidBodySpawnRecorder persistent() { - return persistence(PhysicsBodyPersistenceMode.PERSISTENT); - } - - public void record() { - assertMutable(); - validate(); - sink.accept(bodyKey, - spaceId, - shape, - mass, - bodyType, - positionX, - positionY, - positionZ, - settingsOrDefaults(), - kind, - persistenceMode); - } - - void validate() { - if (spaceId == null) { - throw new IllegalStateException("Spawn command requires a physics space"); - } - if (shape == null) { - throw new IllegalStateException("Spawn command requires a shape"); - } - } - - public void seal() { - sealed = true; - } - - @Nonnull - private RigidBodySpawnSettings settingsOrDefaults() { - return settings != null ? settings : RigidBodySpawnSettings.defaults(); - } - - private void assertMutable() { - if (sealed) { - throw new IllegalStateException("Rigid body spawn recorder is no longer active"); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutableRigidBodySpawnTemplateRecorder.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutableRigidBodySpawnTemplateRecorder.java deleted file mode 100644 index 4f77f637..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/MutableRigidBodySpawnTemplateRecorder.java +++ /dev/null @@ -1,96 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.recorder; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.simulation.batch.RigidBodySpawnTemplateBatch; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.RigidBodySpawnTemplateRecorder; -import java.util.Objects; -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -/** - * Fluent recorder for high-volume rigid body spawns sharing one copied spawn template. - */ -public final class MutableRigidBodySpawnTemplateRecorder implements RigidBodySpawnTemplateRecorder { - - @Nonnull - private final RigidBodySpawnTemplateBatch spawns; - private boolean sealed; - - MutableRigidBodySpawnTemplateRecorder(int expectedBodies, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - spawns = new RigidBodySpawnTemplateBatch(expectedBodies, - spaceId, - shape, - mass, - bodyType, - settings, - kind, - persistenceMode); - } - - @Nonnull - @Override - public RigidBodySpawnTemplateRecorder body(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f position) { - Objects.requireNonNull(position, "position"); - return body(bodyKey, position.x, position.y, position.z); - } - - @Nonnull - @Override - public RigidBodySpawnTemplateRecorder body(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ) { - assertMutable(); - spawns.add(bodyKey, positionX, positionY, positionZ); - return this; - } - - @Nonnull - @Override - public RigidBodySpawnTemplateRecorder body(long bodyKeyMostSignificantBits, - long bodyKeyLeastSignificantBits, - float positionX, - float positionY, - float positionZ) { - assertMutable(); - spawns.add(bodyKeyMostSignificantBits, - bodyKeyLeastSignificantBits, - positionX, - positionY, - positionZ); - return this; - } - - boolean isEmpty() { - return spawns.size() == 0; - } - - @Nonnull - RigidBodySpawnTemplateBatch spawns() { - return spawns; - } - - void seal() { - sealed = true; - } - - private void assertMutable() { - if (sealed) { - throw new IllegalStateException("Rigid body spawn template is already recorded"); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/RigidBodySpawnSink.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/RigidBodySpawnSink.java deleted file mode 100644 index 51b80207..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/recorder/RigidBodySpawnSink.java +++ /dev/null @@ -1,26 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.recorder; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import javax.annotation.Nonnull; - -@FunctionalInterface -interface RigidBodySpawnSink { - - void accept(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - float positionX, - float positionY, - float positionZ, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode); -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/view/BenchmarkSpaceStatsView.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/view/BenchmarkSpaceStatsView.java deleted file mode 100644 index a2f6ab3d..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/view/BenchmarkSpaceStatsView.java +++ /dev/null @@ -1,22 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.view; - -/** - * Copied owner-lane counters used by stress and fall-envelope diagnostics. - */ -public record BenchmarkSpaceStatsView(int bodies, - int dynamicBodies, - int awakeDynamicBodies, - int sleepingDynamicBodies, - int detachedBodies, - int rawBodies, - int worldCollisionBodies, - int belowPlaneBodies, - int belowTerrainBodies, - int belowWorldMinBodies, - int belowVoidBodies, - int terrainBaselineBodies, - int missingTerrainBaselineBodies, - float minTerrainBottomClearance, - float minDynamicBodyY, - float maxDynamicBodyY) { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/view/PhysicsSpaceRuntimeStatsView.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/view/PhysicsSpaceRuntimeStatsView.java deleted file mode 100644 index f0db6312..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/view/PhysicsSpaceRuntimeStatsView.java +++ /dev/null @@ -1,30 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation.view; - -/** - * Copied owner-lane runtime counters for one physics space. - */ -public record PhysicsSpaceRuntimeStatsView(int bodies, - int dynamicBodies, - int awakeDynamicBodies, - int sleepingDynamicBodies, - int staticBodies, - int kinematicBodies, - int entityOwnedBodies, - int detachedBodies, - int worldCollisionBodies, - int planeBodies, - int rawBodies, - int joints, - int contacts, - boolean runtimeStatsAvailable, - int runtimeBodyCount, - int runtimeColliderCount, - int runtimeActiveBodyCount, - int runtimeContactPairCount, - int runtimeContactManifoldCount, - int runtimeContactPointCount, - int runtimeDynamicDynamicContactPairCount, - int runtimeTerrainContactPairCount, - int runtimeActiveIslandCount, - int runtimeJointCount) { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/BodyCommandApplicationSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/BodyCommandApplicationSystem.java new file mode 100644 index 00000000..ea81c322 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/BodyCommandApplicationSystem.java @@ -0,0 +1,274 @@ +package dev.hytalemodding.impulse.core.internal.systems; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.QuerySystem; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource.PendingBodyOperation; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.systems.binding.BodyBindingSystem; +import dev.hytalemodding.impulse.core.plugin.components.BodyCommandComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Drains one-tick body command components into runtime/backend operations. + */ +public final class BodyCommandApplicationSystem extends TickingSystem + implements QuerySystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, BodyBindingSystem.class) + ); + private final Query query = BodyCommandComponent.getComponentType(); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + BiConsumer, CommandBuffer> collector = + (chunk, commandBuffer) -> { + applyCommands(store, runtime, restore, chunk, commandBuffer); + }; + store.forEachChunk(systemIndex, collector); + } + + private static void applyCommands(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull ArchetypeChunk chunk, + @Nonnull CommandBuffer commandBuffer) { + for (int index = 0; index < chunk.size(); index++) { + BodyCommandComponent commands = chunk.getComponent(index, + BodyCommandComponent.getComponentType()); + if (commands == null) { + continue; + } + UUID bodyUuid = PhysicsStoreSystemSupport.rowUuid(chunk, index); + Ref ref = chunk.getReferenceTo(index); + if (PhysicsStoreSystemSupport.isNil(bodyUuid)) { + restore.recordSoftSkip("Body command row has nil UUID"); + commandBuffer.removeComponent(ref, BodyCommandComponent.getComponentType()); + continue; + } + for (BodyCommandComponent.Entry command : commands.entries()) { + applyCommand(store, runtime, restore, commandBuffer, ref, bodyUuid, command); + } + commandBuffer.removeComponent(ref, BodyCommandComponent.getComponentType()); + } + } + + private static void applyCommand(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull CommandBuffer commandBuffer, + @Nonnull Ref ref, + @Nonnull UUID bodyUuid, + @Nonnull BodyCommandComponent.Entry command) { + switch (command.getKind()) { + case WAKE -> runtime.enqueuePendingBodyOperation(PendingBodyOperation.wake(bodyUuid, + ref)); + case SLEEP -> runtime.enqueuePendingBodyOperation(PendingBodyOperation.sleep(bodyUuid, + ref)); + case IMPULSE -> enqueueVector(runtime, ref, bodyUuid, command, PendingBodyOperation.Kind.IMPULSE); + case TORQUE_IMPULSE -> enqueueVector(runtime, + ref, + bodyUuid, + command, + PendingBodyOperation.Kind.TORQUE_IMPULSE); + case FORCE -> enqueueVector(runtime, ref, bodyUuid, command, PendingBodyOperation.Kind.FORCE); + case TORQUE -> enqueueVector(runtime, ref, bodyUuid, command, PendingBodyOperation.Kind.TORQUE); + case SET_TYPE -> applyBodyType(store, + runtime, + restore, + commandBuffer, + ref, + bodyUuid, + command); + case SET_VELOCITY -> applyVelocity(runtime, restore, ref, bodyUuid, command); + case SET_COLLISION_FILTER -> applyCollisionFilter(runtime, + restore, + commandBuffer, + ref, + bodyUuid, + command); + } + } + + private static void applyBodyType(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull CommandBuffer commandBuffer, + @Nonnull Ref ref, + @Nonnull UUID bodyUuid, + @Nonnull BodyCommandComponent.Entry command) { + DynamicsComponent dynamics = PhysicsStoreSystemSupport.component(store, + ref, + DynamicsComponent.getComponentType()); + DynamicsComponent updated = dynamics != null ? dynamics.clone() : new DynamicsComponent(); + updated.setBodyType(command.getBodyType()); + commandBuffer.putComponent(ref, DynamicsComponent.getComponentType(), updated); + + RuntimeBodyBinding binding = runtimeBodyBinding(runtime, ref, bodyUuid, restore, false); + if (binding == null) { + if (command.isActivate()) { + runtime.enqueuePendingBodyOperation(PendingBodyOperation.wake(bodyUuid, ref)); + } + return; + } + binding.backendRuntime().setBodyType(binding.spaceHandle().value(), + binding.bodyHandle().value(), + BackendRuntimeCodes.bodyTypeCode(command.getBodyType())); + updateBodyHitMetadata(runtime, + binding.backendId(), + binding.spaceHandle(), + binding.bodyHandle(), + command.getBodyType()); + if (command.isActivate()) { + runtime.enqueuePendingBodyOperation(PendingBodyOperation.wake(bodyUuid, ref)); + } + } + + private static void applyCollisionFilter(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull CommandBuffer commandBuffer, + @Nonnull Ref ref, + @Nonnull UUID bodyUuid, + @Nonnull BodyCommandComponent.Entry command) { + commandBuffer.putComponent(ref, + CollisionFilterComponent.getComponentType(), + new CollisionFilterComponent(command.getCollisionGroup(), command.getCollisionMask())); + RuntimeBodyBinding binding = runtimeBodyBinding(runtime, ref, bodyUuid, restore, false); + if (binding == null) { + if (command.isActivate()) { + runtime.enqueuePendingBodyOperation(PendingBodyOperation.wake(bodyUuid, ref)); + } + return; + } + binding.backendRuntime().setBodyCollisionFilter(binding.spaceHandle().value(), + binding.bodyHandle().value(), + command.getCollisionGroup(), + command.getCollisionMask()); + if (command.isActivate()) { + runtime.enqueuePendingBodyOperation(PendingBodyOperation.wake(bodyUuid, ref)); + } + } + + private static void applyVelocity(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull Ref ref, + @Nonnull UUID bodyUuid, + @Nonnull BodyCommandComponent.Entry command) { + RuntimeBodyBinding binding = runtimeBodyBinding(runtime, ref, bodyUuid, restore, true); + if (binding == null) { + return; + } + binding.backendRuntime().setBodyVelocity(binding.spaceHandle().value(), + binding.bodyHandle().value(), + command.getX(), + command.getY(), + command.getZ(), + command.getAngularX(), + command.getAngularY(), + command.getAngularZ()); + if (command.isActivate()) { + runtime.enqueuePendingBodyOperation(PendingBodyOperation.wake(bodyUuid, ref)); + } + } + + private static void enqueueVector(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull Ref ref, + @Nonnull UUID bodyUuid, + @Nonnull BodyCommandComponent.Entry command, + @Nonnull PendingBodyOperation.Kind kind) { + runtime.enqueuePendingBodyOperation(PendingBodyOperation.vector(kind, + bodyUuid, + ref, + command.getX(), + command.getY(), + command.getZ(), + command.hasOffset(), + command.getOffsetX(), + command.getOffsetY(), + command.getOffsetZ())); + } + + @Nullable + private static RuntimeBodyBinding runtimeBodyBinding(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull Ref ref, + @Nonnull UUID bodyUuid, + @Nonnull PhysicsRestoreStatusResource restore, + boolean requireBound) { + BackendBodyHandle bodyHandle = runtime.getBodyHandle(ref); + BackendSpaceHandle spaceHandle = runtime.getBodySpaceHandle(ref); + BackendId backendId = runtime.getBodyBackendId(ref); + if (bodyHandle == null || spaceHandle == null || backendId == null) { + if (requireBound) { + restore.recordSoftSkip("Body command target is unbound: " + bodyUuid); + } + return null; + } + PhysicsBackendRuntime backendRuntime = runtime.runtimeForBodyRef(ref); + if (backendRuntime == null) { + restore.recordSoftSkip("Body command backend runtime is missing: " + bodyUuid); + return null; + } + return new RuntimeBodyBinding(backendId, spaceHandle, bodyHandle, backendRuntime); + } + + private static void updateBodyHitMetadata(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull BackendBodyHandle bodyHandle, + @Nonnull PhysicsBodyType bodyType) { + PhysicsRuntimeResource.BodyHitMetadata metadata = + runtime.getBodyHitMetadata(backendId, spaceHandle, bodyHandle.value()); + if (metadata != null) { + runtime.putBodyHitMetadata(backendId, + spaceHandle, + bodyHandle, + metadata.bodyUuid(), + metadata.bodyRef(), + bodyType, + metadata.shapeType()); + } + } + + @Nonnull + @Override + public Query getQuery() { + return query; + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + + private record RuntimeBodyBinding(@Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull BackendBodyHandle bodyHandle, + @Nonnull PhysicsBackendRuntime backendRuntime) { + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/IdentityIndexSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/IdentityIndexSystem.java new file mode 100644 index 00000000..10b106e3 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/IdentityIndexSystem.java @@ -0,0 +1,60 @@ +package dev.hytalemodding.impulse.core.internal.systems; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.QuerySystem; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; + +/** + * Rebuilds durable UUID indexes for boundary lookup. + */ +public final class IdentityIndexSystem extends TickingSystem + implements QuerySystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, PersistenceHydrationSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + store.getExternalData().clearUuidIndex(); + BiConsumer, CommandBuffer> collector = + (chunk, _) -> indexChunk(store, chunk); + store.forEachChunk(systemIndex, collector); + } + + private static void indexChunk(@Nonnull Store store, + @Nonnull ArchetypeChunk chunk) { + for (int index = 0; index < chunk.size(); index++) { + UUID uuid = PhysicsStoreSystemSupport.rowUuid(chunk, index); + if (PhysicsStoreSystemSupport.isNil(uuid)) { + continue; + } + Ref ref = chunk.getReferenceTo(index); + store.getExternalData().putRefForUUID(uuid, ref); + } + } + + @Nonnull + @Override + public Query getQuery() { + return PhysicsStoreSystemSupport.uuidQuery(); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/PersistenceHydrationSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/PersistenceHydrationSystem.java new file mode 100644 index 00000000..60b47bd6 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/PersistenceHydrationSystem.java @@ -0,0 +1,51 @@ +package dev.hytalemodding.impulse.core.internal.systems; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.persistence.PhysicsStoreHolderStorage; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsEventResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import java.util.Set; +import javax.annotation.Nonnull; + +/** + * Rehydrates persisted PhysicsStore holder rows before backend mutation is allowed. + */ +public final class PersistenceHydrationSystem extends TickingSystem { + + private static final Set> DEPENDENCIES = Set.of(); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + if (restore.isFailed() || restore.isHydrated()) { + return; + } + try { + prepareTransientRestoreState(store); + PhysicsStoreHolderStorage.load(store); + restore.markComplete(); + restore.markHydrated(); + } catch (RuntimeException exception) { + restore.markFailed(exception.getMessage()); + } + } + + private static void prepareTransientRestoreState(@Nonnull Store store) { + store.getResource(PhysicsRuntimeResource.getResourceType()).destroyBackendBindings(); + store.getExternalData().clearUuidIndex(); + store.getResource(PhysicsSnapshotResource.getResourceType()).clear(); + store.getResource(PhysicsEventResource.getResourceType()).clear(); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/PhysicsStoreQueuedReadSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/PhysicsStoreQueuedReadSystem.java new file mode 100644 index 00000000..457acee1 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/PhysicsStoreQueuedReadSystem.java @@ -0,0 +1,54 @@ +package dev.hytalemodding.impulse.core.internal.systems; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStoreReadQueueResource; +import dev.hytalemodding.impulse.core.internal.systems.publication.CompletedStepPublicationSystem; + +import java.util.List; +import java.util.Set; +import javax.annotation.Nonnull; + +/** + * Resolves queued live backend reads on the owning PhysicsStore world thread. + */ +public final class PhysicsStoreQueuedReadSystem extends TickingSystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, CompletedStepPublicationSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + PhysicsStoreReadQueueResource queue = store.getResource( + PhysicsStoreReadQueueResource.getResourceType()); + List> reads = queue.drain(); + if (reads.isEmpty()) { + return; + } + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + if (restore.isFailed()) { + IllegalStateException failure = new IllegalStateException( + "PhysicsStore restore failed: " + restore.getFailureMessage()); + for (PhysicsStoreReadQueueResource.QueuedRead read : reads) { + read.fail(failure); + } + return; + } + for (PhysicsStoreReadQueueResource.QueuedRead read : reads) { + read.complete(store); + } + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/PhysicsStoreSystemSupport.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/PhysicsStoreSystemSupport.java new file mode 100644 index 00000000..fbc946b4 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/PhysicsStoreSystemSupport.java @@ -0,0 +1,57 @@ +package dev.hytalemodding.impulse.core.internal.systems; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public final class PhysicsStoreSystemSupport { + + static final UUID NIL_UUID = new UUID(0L, 0L); + private PhysicsStoreSystemSupport() { + } + + @Nonnull + static ComponentType uuidType() { + return UuidComponent.getComponentType(); + } + + @Nonnull + public static Query uuidQuery() { + return uuidType(); + } + + @Nonnull + public static UUID rowUuid(@Nonnull ArchetypeChunk chunk, int index) { + UuidComponent uuid = chunk.getComponent(index, uuidType()); + return uuid != null ? uuid.getUuid() : NIL_UUID; + } + + @Nonnull + static UUID rowUuid(@Nonnull Ref ref) { + UuidComponent uuid = component(ref.getStore(), ref, uuidType()); + return uuid != null ? uuid.getUuid() : NIL_UUID; + } + + public static boolean isNil(@Nonnull UUID uuid) { + return NIL_UUID.equals(uuid); + } + + @Nullable + public static > C component( + @Nonnull Store store, + @Nullable Ref ref, + @Nonnull ComponentType type) { + if (ref == null || !ref.isValid()) { + return null; + } + return store.getComponent(ref, type); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/SpaceSettingsApplicationSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/SpaceSettingsApplicationSystem.java new file mode 100644 index 00000000..99f647db --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/SpaceSettingsApplicationSystem.java @@ -0,0 +1,141 @@ +package dev.hytalemodding.impulse.core.internal.systems; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.capability.PhysicsActivationTuning; +import dev.hytalemodding.impulse.api.capability.PhysicsCapabilityId; +import dev.hytalemodding.impulse.api.capability.PhysicsSolverTuning; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.systems.binding.SpaceBindingSystem; +import dev.hytalemodding.impulse.core.plugin.components.ExtensionSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.components.SolverSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsExtensionSettingValue; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsBackendExtensionId; +import dev.hytalemodding.impulse.api.BackendId; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3f; + +/** + * Applies changed backend-facing space settings after space binding and before body binding. + */ +public final class SpaceSettingsApplicationSystem extends TickingSystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, SpaceBindingSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + if (restore.isFailed()) { + return; + } + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + Set> pending = runtime.drainPendingSpaceSettings(); + if (pending.isEmpty()) { + return; + } + for (Ref ref : pending) { + if (ref == null || !ref.isValid()) { + continue; + } + UUID spaceUuid = PhysicsStoreSystemSupport.rowUuid(ref); + if (PhysicsStoreSystemSupport.isNil(spaceUuid)) { + continue; + } + try { + applyIfBound(store, runtime, ref, spaceUuid); + } catch (RuntimeException exception) { + restore.markFailed("PhysicsStore space " + spaceUuid + + " failed backend settings application: " + exception.getMessage()); + return; + } + } + } + + static boolean applyIfBound(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull Ref ref, + @Nonnull UUID spaceUuid) { + BackendSpaceHandle handle = runtime.getSpaceHandle(ref); + if (handle == null) { + return false; + } + PhysicsBackendRuntime backendRuntime = backendRuntime(runtime, ref); + if (backendRuntime == null) { + return false; + } + SpaceComponent space = store.getComponent(ref, SpaceComponent.getComponentType()); + SolverSettingsComponent solverSettings = store.getComponent(ref, + SolverSettingsComponent.getComponentType()); + ExtensionSettingsComponent extensionSettings = store.getComponent(ref, + ExtensionSettingsComponent.getComponentType()); + if (space != null) { + Vector3f gravity = space.getGravity(); + backendRuntime.setGravity(handle.value(), gravity.x, gravity.y, gravity.z); + } + applyBackendSettings(backendRuntime, + handle, + solverSettings != null ? solverSettings : new SolverSettingsComponent(), + extensionSettings); + return true; + } + + public static void applyBackendSettings(@Nonnull PhysicsBackendRuntime runtime, + @Nonnull BackendSpaceHandle handle, + @Nonnull SolverSettingsComponent solverSettings, + @Nullable ExtensionSettingsComponent extensionSettings) { + if (runtime.supportsSolverTuning(handle.value())) { + runtime.applySolverTuning(handle.value(), + new PhysicsSolverTuning(solverSettings.getSolverIterations(), + solverSettings.getStabilizationIterations())); + } + if (runtime.supportsActivationTuning(handle.value())) { + runtime.applyActivationTuning(handle.value(), + new PhysicsActivationTuning(solverSettings.getDynamicSleepLinearThreshold(), + solverSettings.getDynamicSleepAngularThreshold(), + solverSettings.getDynamicSleepTimeUntilSleep())); + } + if (extensionSettings == null) { + return; + } + Map> settingsByExtension = + extensionSettings.asMap(); + for (PhysicsBackendExtensionId extensionId : settingsByExtension.keySet()) { + Map settings = settingsByExtension.get(extensionId); + runtime.applyExtensionSettings(handle.value(), + new PhysicsCapabilityId(extensionId.value()), + consumer -> settings.forEach((key, value) -> consumer.accept(key, value.value()))); + } + } + + @Nullable + private static PhysicsBackendRuntime backendRuntime(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull Ref spaceRef) { + BackendId backendId = runtime.getSpaceBackendId(spaceRef); + if (backendId == null) { + return null; + } + return runtime.getRuntime(backendId); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/StaleBodyRemovalSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/StaleBodyRemovalSystem.java new file mode 100644 index 00000000..5df2edcb --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/StaleBodyRemovalSystem.java @@ -0,0 +1,250 @@ +package dev.hytalemodding.impulse.core.internal.systems; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsStoreRowCleanup; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsStoreRowCleanup.BodyEntityRemoval; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource.BodySnapshotMetadata; +import dev.hytalemodding.impulse.core.internal.systems.binding.JointBindingSystem; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentLinkedQueue; +import javax.annotation.Nonnull; + +/** + * Removes backend bodies after their authoritative PhysicsStore body entity is gone. + */ +public final class StaleBodyRemovalSystem extends TickingSystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, JointBindingSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + if (restore.isFailed()) { + return; + } + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + removeStaleBodies(store, runtime, restore); + } + + private static void removeStaleBodies(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsRestoreStatusResource restore) { + List staleBodies = new ArrayList<>(); + runtime.forEachRuntimeSpaceBinding((_, backendId, spaceHandle, backendRuntime) -> + runtime.forEachBodyHandle(backendId, + spaceHandle, + bodyId -> collectStaleBody(store, + runtime, + restore, + staleBodies, + backendId, + spaceHandle, + backendRuntime, + bodyId))); + if (restore.isFailed()) { + return; + } + Set staleBodyUuids = new ObjectOpenHashSet<>(); + staleBodies.forEach(body -> staleBodyUuids.add(body.bodyUuid())); + if (!removeDependentJoints(store, runtime, restore, staleBodyUuids)) { + return; + } + List orderedBodies = currentBodyRefs(store, staleBodies); + List bodyEntityRemovals = new ArrayList<>(orderedBodies.size()); + for (BoundBody body : orderedBodies) { + try { + PhysicsStoreRowCleanup.removeRuntimeBody(store, + runtime, + body.bodyUuid(), + body.bodyRef(), + body.backendRuntime()); + } catch (RuntimeException exception) { + removeBodyEntities(store, bodyEntityRemovals); + restore.markFailed("PhysicsStore body " + body.bodyUuid() + + " failed backend removal: " + exception.getMessage()); + return; + } + bodyEntityRemovals.add(new BodyEntityRemoval(body.bodyUuid(), body.bodyRef())); + } + removeBodyEntities(store, bodyEntityRemovals); + } + + private static boolean removeDependentJoints(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull Set staleBodyUuids) { + if (staleBodyUuids.isEmpty()) { + return true; + } + List joints = collectDependentJoints(store, staleBodyUuids); + joints.sort((first, second) -> Integer.compare(second.ref().getIndex(), + first.ref().getIndex())); + boolean removedAny = false; + for (BoundJoint joint : joints) { + try { + PhysicsStoreRowCleanup.removeRuntimeJoint(store, + runtime, + joint.jointUuid(), + joint.ref()); + } catch (RuntimeException exception) { + restore.markFailed("PhysicsStore joint " + joint.jointUuid() + + " failed backend removal: " + exception.getMessage()); + return false; + } + if (joint.removeRow() && joint.ref().isValid()) { + PhysicsStoreRowCleanup.removeJointEntity(store, joint.jointUuid(), joint.ref()); + removedAny = true; + } + } + if (removedAny) { + PhysicsStoreRowCleanup.refreshIdentityAndRuntimeRefs(store); + } + return true; + } + + private static void removeBodyEntities(@Nonnull Store store, + @Nonnull List removals) { + if (removals.isEmpty()) { + return; + } + PhysicsStoreRowCleanup.removeBodyEntities(store, removals); + PhysicsStoreRowCleanup.refreshIdentityAndRuntimeRefs(store); + } + + @Nonnull + private static List currentBodyRefs(@Nonnull Store store, + @Nonnull List staleBodies) { + List currentBodies = new ArrayList<>(staleBodies.size()); + for (BoundBody body : staleBodies) { + Ref bodyRef = store.getExternalData().getRefFromUUID(body.bodyUuid()); + if (bodyRef != null && (bodyRef.getStore() != store || !bodyRef.isValid())) { + bodyRef = null; + } + currentBodies.add(new BoundBody(body.bodyUuid(), + bodyRef != null ? bodyRef : body.bodyRef(), + body.backendRuntime())); + } + currentBodies.sort((first, second) -> Integer.compare(second.bodyRef().getIndex(), + first.bodyRef().getIndex())); + return currentBodies; + } + + @Nonnull + private static List collectDependentJoints(@Nonnull Store store, + @Nonnull Set staleBodyUuids) { + ConcurrentLinkedQueue joints = new ConcurrentLinkedQueue<>(); + store.forEachEntityParallel(UuidComponent.getComponentType(), (index, chunk, _) -> { + JointComponent joint = chunk.getComponent(index, JointComponent.getComponentType()); + if (joint == null + || (!staleBodyUuids.contains(joint.getBodyAUuid()) + && !staleBodyUuids.contains(joint.getBodyBUuid()))) { + return; + } + UUID jointUuid = PhysicsStoreSystemSupport.rowUuid(chunk, index); + if (PhysicsStoreSystemSupport.isNil(jointUuid)) { + return; + } + boolean removeRow = shouldRemoveJointRow(store, joint, staleBodyUuids); + joints.add(new BoundJoint(jointUuid, chunk.getReferenceTo(index), removeRow)); + }); + return new ArrayList<>(joints); + } + + private static boolean shouldRemoveJointRow(@Nonnull Store store, + @Nonnull JointComponent joint, + @Nonnull Set staleBodyUuids) { + return endpointRemoved(store, joint.getBodyAUuid(), joint.getBodyARef(), staleBodyUuids) + || endpointRemoved(store, joint.getBodyBUuid(), joint.getBodyBRef(), staleBodyUuids); + } + + private static boolean endpointRemoved(@Nonnull Store store, + @Nonnull UUID bodyUuid, + Ref currentRef, + @Nonnull Set staleBodyUuids) { + if (!staleBodyUuids.contains(bodyUuid)) { + return false; + } + UuidComponent currentUuid = PhysicsStoreSystemSupport.component(store, + currentRef, + UuidComponent.getComponentType()); + if (currentUuid != null && bodyUuid.equals(currentUuid.getUuid())) { + return false; + } + Ref indexed = store.getExternalData().getRefFromUUID(bodyUuid); + return indexed == null || indexed.getStore() != store || !indexed.isValid(); + } + + private static void collectStaleBody(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull List staleBodies, + @Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull PhysicsBackendRuntime backendRuntime, + long bodyId) { + BodySnapshotMetadata metadata = runtime.getBodySnapshotMetadata(backendId, + spaceHandle, + bodyId); + if (metadata == null) { + restore.markFailed("PhysicsStore backend body " + bodyId + + " has no runtime snapshot metadata"); + return; + } + Ref bodyRef = metadata.bodyRef(); + UuidComponent currentUuid = PhysicsStoreSystemSupport.component(store, + bodyRef, + UuidComponent.getComponentType()); + if (currentUuid == null || !metadata.bodyUuid().equals(currentUuid.getUuid())) { + bodyRef = store.getExternalData().getRefFromUUID(metadata.bodyUuid()); + } + if (bodyRef == null) { + bodyRef = metadata.bodyRef(); + } + BodyComponent body = PhysicsStoreSystemSupport.component(store, + bodyRef, + BodyComponent.getComponentType()); + if (body != null) { + return; + } + staleBodies.add(new BoundBody(metadata.bodyUuid(), + bodyRef, + backendRuntime)); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + + private record BoundBody(@Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef, + @Nonnull PhysicsBackendRuntime backendRuntime) { + } + + private record BoundJoint(@Nonnull UUID jointUuid, + @Nonnull Ref ref, + boolean removeRow) { + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/binding/BodyBindingSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/binding/BodyBindingSystem.java new file mode 100644 index 00000000..646a7164 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/binding/BodyBindingSystem.java @@ -0,0 +1,341 @@ +package dev.hytalemodding.impulse.core.internal.systems.binding; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.QuerySystem; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkStoreTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionPayloadResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionPayload; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.systems.PhysicsStoreSystemSupport; +import dev.hytalemodding.impulse.core.internal.systems.SpaceSettingsApplicationSystem; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +/** + * Creates backend bodies from authoritative PhysicsStore body entities. + */ +public final class BodyBindingSystem extends TickingSystem + implements QuerySystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, SpaceBindingSystem.class), + new SystemDependency<>(Order.AFTER, SpaceSettingsApplicationSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + if (restore.isFailed()) { + return; + } + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + PhysicsChunkCollisionPayloadResource chunkCollisionPayloads = + PhysicsChunkStoreTypes.collisionPayloadsIfPresent(store); + BiConsumer, CommandBuffer> collector = + (chunk, commandBuffer) -> bindBodies(store, + runtime, + chunkCollisionPayloads, + restore, + commandBuffer, + chunk); + store.forEachChunk(systemIndex, collector); + } + + private static void bindBodies(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nullable PhysicsChunkCollisionPayloadResource chunkCollisionPayloads, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull CommandBuffer commandBuffer, + @Nonnull ArchetypeChunk chunk) { + for (int index = 0; index < chunk.size(); index++) { + BodyComponent body = chunk.getComponent(index, BodyComponent.getComponentType()); + if (body == null) { + continue; + } + UUID bodyUuid = PhysicsStoreSystemSupport.rowUuid(chunk, index); + if (PhysicsStoreSystemSupport.isNil(bodyUuid)) { + continue; + } + Ref bodyRef = chunk.getReferenceTo(index); + if (runtime.getBodyHandle(bodyRef) != null) { + continue; + } + bindBody(runtime, + store, + chunkCollisionPayloads, + restore, + commandBuffer, + bodyRef, + bodyUuid, + body, + chunk.getComponent(index, DynamicsComponent.getComponentType()), + chunk.getComponent(index, TargetComponent.getComponentType()), + chunk.getComponent(index, ColliderComponent.getComponentType()), + chunk.getComponent(index, ShapeComponent.getComponentType()), + chunk.getComponent(index, MaterialComponent.getComponentType()), + chunk.getComponent(index, CollisionFilterComponent.getComponentType())); + } + } + + private static void bindBody(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull Store store, + @Nullable PhysicsChunkCollisionPayloadResource chunkCollisionPayloads, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull CommandBuffer commandBuffer, + @Nonnull Ref bodyRef, + @Nonnull UUID bodyUuid, + @Nonnull BodyComponent body, + @Nullable DynamicsComponent dynamics, + @Nullable TargetComponent target, + @Nullable ColliderComponent collider, + @Nullable ShapeComponent shape, + @Nullable MaterialComponent material, + @Nullable CollisionFilterComponent filter) { + Ref spaceRef = resolveSpaceRef(store, body); + BackendSpaceHandle spaceHandle = spaceRef != null ? runtime.getSpaceHandle(spaceRef) : null; + if (spaceHandle == null) { + restore.recordSoftSkip("Body references unbound space: " + bodyUuid); + return; + } + BackendId backendId = runtime.getSpaceBackendId(spaceRef); + if (backendId == null) { + restore.recordSoftSkip("Body references missing backend id: " + bodyUuid); + return; + } + PhysicsBackendRuntime backendRuntime = runtimeForSpace(runtime, spaceRef); + if (backendRuntime == null) { + restore.recordSoftSkip("Body references missing backend runtime: " + bodyUuid); + return; + } + if (collider == null || shape == null || material == null || filter == null) { + restore.recordSoftSkip("Body aggregate is missing collider data: " + bodyUuid); + return; + } + if (PhysicsChunkStoreTypes.shouldDeferChunkCollisionRestore(store, + commandBuffer, + bodyRef, + body, + dynamics, + restore)) { + return; + } + DynamicsComponent bodyDynamics = dynamics != null ? dynamics : new DynamicsComponent(); + TargetComponent initialTarget = target != null ? target : new TargetComponent(); + Vector3f position = target != null ? initialTarget.getPosition() : new Vector3f(); + Quaternionf rotation = target != null ? initialTarget.getRotation() : new Quaternionf(); + PhysicsBodyType bodyType = bodyDynamics.getBodyType(); + float mass = bodyType == PhysicsBodyType.DYNAMIC ? bodyDynamics.getMass() : 0.0f; + long bodyId = Long.MIN_VALUE; + boolean bodyCreatedWithInitialState = false; + try { + if (shape.getShapeType() == ShapeType.VOXELS) { + if (bodyType != PhysicsBodyType.STATIC) { + restore.recordSoftSkip("Voxel body must be static: " + bodyUuid); + return; + } + bodyId = createVoxelBody(chunkCollisionPayloads, + backendRuntime, + spaceHandle, + shape, + material, + filter, + position); + if (bodyId == Long.MIN_VALUE) { + restore.recordSoftSkip("Voxel body payload is missing or unsupported: " + + bodyUuid); + return; + } + } else { + bodyId = backendRuntime.createBodyWithInitialState(spaceHandle.value(), + BackendRuntimeCodes.shapeTypeCode(shape.getShapeType()), + shape.getHalfExtentX(), + shape.getHalfExtentY(), + shape.getHalfExtentZ(), + shape.getRadius(), + shape.getHalfHeight(), + BackendRuntimeCodes.axisCode(shape.getAxis()), + shape.getGroundY(), + mass, + BackendRuntimeCodes.bodyTypeCode(bodyType), + position.x, + position.y, + position.z, + rotation.x, + rotation.y, + rotation.z, + rotation.w, + bodyDynamics.getLinearDamping(), + bodyDynamics.getAngularDamping(), + material.getFriction(), + material.getRestitution(), + filter.getCollisionGroup(), + filter.getCollisionMask(), + collider.isSensor(), + bodyDynamics.isContinuousCollisionEnabled()); + bodyCreatedWithInitialState = true; + } + BackendBodyHandle bodyHandle = new BackendBodyHandle(bodyId); + if (!bodyCreatedWithInitialState + && bodyDynamics.isContinuousCollisionEnabled() + && backendRuntime.supportsContinuousCollision(spaceHandle.value())) { + backendRuntime.setBodyContinuousCollision(spaceHandle.value(), bodyId, true); + } + applyInitialTargetState(backendRuntime, spaceHandle, bodyHandle, bodyType, target); + runtime.putBodyHandle(bodyRef, spaceRef, spaceHandle, bodyHandle); + runtime.putBodySnapshotMetadata(backendId, + spaceHandle, + bodyHandle, + bodyUuid, + bodyRef, + body.getSpaceUuid()); + runtime.putBodyHitMetadata(backendId, + spaceHandle, + bodyHandle, + bodyUuid, + bodyRef, + bodyType, + shape.getShapeType()); + } catch (RuntimeException exception) { + if (bodyId != Long.MIN_VALUE) { + try { + backendRuntime.removeBody(spaceHandle.value(), bodyId); + } catch (RuntimeException ignored) { + // Preserve the original backend failure as the restore status. + } + } + restore.markFailed("PhysicsStore body " + bodyUuid + + " failed backend binding: " + exception.getMessage()); + } + } + + private static long createVoxelBody(@Nullable PhysicsChunkCollisionPayloadResource chunkCollisionPayloads, + @Nonnull PhysicsBackendRuntime backendRuntime, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull ShapeComponent shape, + @Nonnull MaterialComponent material, + @Nonnull CollisionFilterComponent filter, + @Nonnull Vector3f position) { + String payloadKey = shape.getResourceKey(); + if (payloadKey.isBlank() || !backendRuntime.supportsVoxelTerrain(spaceHandle.value())) { + return Long.MIN_VALUE; + } + if (chunkCollisionPayloads == null) { + return Long.MIN_VALUE; + } + ChunkCollisionPayload payload = chunkCollisionPayloads.get(payloadKey); + if (payload == null || !payload.hasFullCubeVoxels()) { + return Long.MIN_VALUE; + } + try { + return backendRuntime.createVoxelTerrain(spaceHandle.value(), + payload.voxelSizeX(), + payload.voxelSizeY(), + payload.voxelSizeZ(), + payload.voxelCoordinates(), + position.x, + position.y, + position.z, + material.getFriction(), + material.getRestitution(), + filter.getCollisionGroup(), + filter.getCollisionMask()); + } catch (UnsupportedOperationException exception) { + return Long.MIN_VALUE; + } + } + + private static void applyInitialTargetState(@Nonnull PhysicsBackendRuntime backendRuntime, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull BackendBodyHandle bodyHandle, + @Nonnull PhysicsBodyType bodyType, + @Nullable TargetComponent target) { + if (target == null) { + return; + } + if (target.isVelocityEnabled()) { + Vector3f linearVelocity = target.getLinearVelocity(); + Vector3f angularVelocity = target.getAngularVelocity(); + backendRuntime.setBodyVelocity(spaceHandle.value(), + bodyHandle.value(), + linearVelocity.x, + linearVelocity.y, + linearVelocity.z, + angularVelocity.x, + angularVelocity.y, + angularVelocity.z); + } + if (target.isActivate()) { + backendRuntime.activateBody(spaceHandle.value(), bodyHandle.value()); + } else if (bodyType == PhysicsBodyType.DYNAMIC) { + backendRuntime.sleepBody(spaceHandle.value(), bodyHandle.value()); + } + } + + @Nullable + private static Ref resolveSpaceRef(@Nonnull Store store, + @Nonnull BodyComponent body) { + Ref spaceRef = body.getSpaceRef(); + UuidComponent uuid = PhysicsStoreSystemSupport.component(store, + spaceRef, + UuidComponent.getComponentType()); + if (uuid == null || !body.getSpaceUuid().equals(uuid.getUuid())) { + spaceRef = store.getExternalData().getRefFromUUID(body.getSpaceUuid()); + } + if (spaceRef != null && (spaceRef.getStore() != store || !spaceRef.isValid())) { + spaceRef = null; + } + body.setSpaceRef(spaceRef); + return spaceRef; + } + + @Nullable + private static PhysicsBackendRuntime runtimeForSpace(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull Ref spaceRef) { + var backendId = runtime.getSpaceBackendId(spaceRef); + return backendId != null ? runtime.getRuntime(backendId) : null; + } + + @Nonnull + @Override + public Query getQuery() { + return PhysicsStoreSystemSupport.uuidQuery(); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/binding/ColliderBindingSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/binding/ColliderBindingSystem.java new file mode 100644 index 00000000..520eb48a --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/binding/ColliderBindingSystem.java @@ -0,0 +1,34 @@ +package dev.hytalemodding.impulse.core.internal.systems.binding; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import java.util.Set; +import javax.annotation.Nonnull; + +/** + * Reserved for multi-collider and compound-shape reconciliation. + */ +public final class ColliderBindingSystem extends TickingSystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, BodyBindingSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + /* + * V1 body binding creates one backend body from the first collider row. Compound collider + * updates stay explicit here so they do not silently become EntityStore authority again. + */ + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/binding/JointBindingSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/binding/JointBindingSystem.java new file mode 100644 index 00000000..f3d2d2ba --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/binding/JointBindingSystem.java @@ -0,0 +1,253 @@ +package dev.hytalemodding.impulse.core.internal.systems.binding; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.QuerySystem; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.runtime.BackendJointType; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendJointHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.systems.PhysicsStoreSystemSupport; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3f; + +/** + * Binds joint entities once both endpoint bodies are bound. + */ +public final class JointBindingSystem extends TickingSystem + implements QuerySystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, ColliderBindingSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + if (restore.isFailed()) { + return; + } + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + BiConsumer, CommandBuffer> collector = + (chunk, _) -> bindChunk(store, runtime, restore, chunk); + store.forEachChunk(systemIndex, collector); + } + + private static void bindChunk(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull ArchetypeChunk chunk) { + for (int index = 0; index < chunk.size(); index++) { + JointComponent joint = chunk.getComponent(index, JointComponent.getComponentType()); + if (joint == null) { + continue; + } + UUID jointUuid = PhysicsStoreSystemSupport.rowUuid(chunk, index); + if (PhysicsStoreSystemSupport.isNil(jointUuid)) { + continue; + } + Ref jointRef = chunk.getReferenceTo(index); + if (!joint.isEnabled()) { + removeJoint(runtime, jointRef); + continue; + } + BackendJointHandle existing = runtime.getJointHandle(jointRef); + if (existing != null) { + if (!endpointsBound(store, runtime, joint)) { + removeJoint(runtime, jointRef); + } + continue; + } + bindJoint(store, runtime, restore, jointRef, jointUuid, joint); + } + } + + private static void bindJoint(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull Ref jointRef, + @Nonnull UUID jointUuid, + @Nonnull JointComponent joint) { + Ref spaceRef = resolveSpaceRef(store, joint); + Ref bodyARef = resolveBodyARef(store, joint); + Ref bodyBRef = resolveBodyBRef(store, joint); + BackendSpaceHandle spaceHandle = spaceRef != null ? runtime.getSpaceHandle(spaceRef) : null; + BackendBodyHandle bodyA = bodyARef != null ? runtime.getBodyHandle(bodyARef) : null; + BackendBodyHandle bodyB = bodyBRef != null ? runtime.getBodyHandle(bodyBRef) : null; + BackendSpaceHandle bodyASpace = bodyARef != null ? runtime.getBodySpaceHandle(bodyARef) : null; + BackendSpaceHandle bodyBSpace = bodyBRef != null ? runtime.getBodySpaceHandle(bodyBRef) : null; + BackendId backendId = spaceRef != null ? runtime.getSpaceBackendId(spaceRef) : null; + BackendId bodyABackendId = bodyARef != null ? runtime.getBodyBackendId(bodyARef) : null; + BackendId bodyBBackendId = bodyBRef != null ? runtime.getBodyBackendId(bodyBRef) : null; + PhysicsBackendRuntime backendRuntime = backendId != null ? runtime.getRuntime(backendId) : null; + if (spaceHandle == null || bodyA == null || bodyB == null || backendRuntime == null) { + restore.recordSoftSkip("Joint references unbound endpoint: " + jointUuid); + return; + } + if (bodyASpace == null + || bodyBSpace == null + || bodyABackendId == null + || bodyBBackendId == null + || !bodyABackendId.equals(backendId) + || !bodyBBackendId.equals(backendId) + || bodyASpace.value() != spaceHandle.value() + || bodyBSpace.value() != spaceHandle.value()) { + restore.recordSoftSkip("Joint endpoints are not in the joint space: " + jointUuid); + return; + } + Vector3f anchorA = joint.getAnchorA(); + Vector3f anchorB = joint.getAnchorB(); + Vector3f axis = joint.getAxis(); + long jointId = Long.MIN_VALUE; + try { + jointId = backendRuntime.createJoint(spaceHandle.value(), + BackendRuntimeCodes.jointTypeCode(BackendJointType.valueOf(joint.getType().name())), + bodyA.value(), + bodyB.value(), + anchorA.x, + anchorA.y, + anchorA.z, + anchorB.x, + anchorB.y, + anchorB.z, + axis.x, + axis.y, + axis.z, + joint.getSpringRestLength(), + joint.getSpringStiffness(), + joint.getSpringDamping(), + joint.getLowerLimit(), + joint.getUpperLimit(), + joint.isMotorEnabled(), + joint.getMotorTargetVelocity(), + joint.getMotorMaxForce()); + BackendJointHandle handle = new BackendJointHandle(jointId); + runtime.putJointHandle(jointRef, spaceRef, spaceHandle, handle); + runtime.putJointMetadata(backendId, spaceHandle, handle, jointUuid, jointRef); + } catch (RuntimeException exception) { + if (jointId != Long.MIN_VALUE) { + try { + backendRuntime.removeJoint(spaceHandle.value(), jointId); + } catch (RuntimeException ignored) { + // Preserve the original backend failure as the restore status. + } + } + restore.markFailed("PhysicsStore joint " + jointUuid + + " failed backend binding: " + exception.getMessage()); + } + } + + private static boolean endpointsBound(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull JointComponent joint) { + Ref spaceRef = resolveSpaceRef(store, joint); + Ref bodyARef = resolveBodyARef(store, joint); + Ref bodyBRef = resolveBodyBRef(store, joint); + BackendSpaceHandle spaceHandle = spaceRef != null ? runtime.getSpaceHandle(spaceRef) : null; + BackendSpaceHandle bodyASpace = bodyARef != null ? runtime.getBodySpaceHandle(bodyARef) : null; + BackendSpaceHandle bodyBSpace = bodyBRef != null ? runtime.getBodySpaceHandle(bodyBRef) : null; + BackendId backendId = spaceRef != null ? runtime.getSpaceBackendId(spaceRef) : null; + BackendId bodyABackendId = bodyARef != null ? runtime.getBodyBackendId(bodyARef) : null; + BackendId bodyBBackendId = bodyBRef != null ? runtime.getBodyBackendId(bodyBRef) : null; + return spaceHandle != null + && backendId != null + && bodyARef != null + && bodyBRef != null + && runtime.getBodyHandle(bodyARef) != null + && runtime.getBodyHandle(bodyBRef) != null + && bodyASpace != null + && bodyBSpace != null + && backendId.equals(bodyABackendId) + && backendId.equals(bodyBBackendId) + && bodyASpace.value() == spaceHandle.value() + && bodyBSpace.value() == spaceHandle.value(); + } + + @Nullable + private static Ref resolveSpaceRef(@Nonnull Store store, + @Nonnull JointComponent joint) { + Ref spaceRef = resolveRef(store, joint.getSpaceUuid(), joint.getSpaceRef()); + joint.setSpaceRef(spaceRef); + return spaceRef; + } + + @Nullable + private static Ref resolveBodyARef(@Nonnull Store store, + @Nonnull JointComponent joint) { + Ref bodyRef = resolveRef(store, joint.getBodyAUuid(), joint.getBodyARef()); + joint.setBodyARef(bodyRef); + return bodyRef; + } + + @Nullable + private static Ref resolveBodyBRef(@Nonnull Store store, + @Nonnull JointComponent joint) { + Ref bodyRef = resolveRef(store, joint.getBodyBUuid(), joint.getBodyBRef()); + joint.setBodyBRef(bodyRef); + return bodyRef; + } + + @Nullable + private static Ref resolveRef(@Nonnull Store store, + @Nonnull UUID uuid, + @Nullable Ref current) { + if (PhysicsStoreSystemSupport.isNil(uuid)) { + return null; + } + UuidComponent currentUuid = PhysicsStoreSystemSupport.component(store, + current, + UuidComponent.getComponentType()); + Ref resolved = currentUuid != null && uuid.equals(currentUuid.getUuid()) + ? current + : store.getExternalData().getRefFromUUID(uuid); + return resolved != null && resolved.getStore() == store && resolved.isValid() + ? resolved + : null; + } + + private static void removeJoint(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull Ref jointRef) { + BackendJointHandle handle = runtime.getJointHandle(jointRef); + if (handle == null) { + return; + } + BackendSpaceHandle spaceHandle = runtime.getJointSpaceHandle(jointRef); + PhysicsBackendRuntime backendRuntime = runtime.runtimeForJointRef(jointRef); + if (spaceHandle != null && backendRuntime != null) { + backendRuntime.removeJoint(spaceHandle.value(), handle.value()); + } + runtime.removeJointHandle(jointRef); + } + + @Nonnull + @Override + public Query getQuery() { + return PhysicsStoreSystemSupport.uuidQuery(); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/binding/SpaceBindingSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/binding/SpaceBindingSystem.java new file mode 100644 index 00000000..a152197f --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/binding/SpaceBindingSystem.java @@ -0,0 +1,187 @@ +package dev.hytalemodding.impulse.core.internal.systems.binding; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.QuerySystem; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.ImpulseBackendRegistry; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldSettingsResource; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.systems.IdentityIndexSystem; +import dev.hytalemodding.impulse.core.internal.systems.PhysicsStoreSystemSupport; +import dev.hytalemodding.impulse.core.internal.systems.SpaceSettingsApplicationSystem; +import dev.hytalemodding.impulse.core.plugin.components.ExtensionSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.components.SolverSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3f; + +/** + * Binds SpaceComponent rows to backend runtime spaces. + */ +public final class SpaceBindingSystem extends TickingSystem + implements QuerySystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, IdentityIndexSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + if (restore.isFailed()) { + return; + } + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + PhysicsSpaceCompatibilityIndexResource compatibility = store.getResource( + PhysicsSpaceCompatibilityIndexResource.getResourceType()); + PhysicsStepMode stepMode = store.getResource(PhysicsWorldSettingsResource.getResourceType()) + .getSettings() + .getStepMode(); + BiConsumer, CommandBuffer> collector = + (chunk, _) -> bindChunk(runtime, compatibility, restore, stepMode, chunk); + store.forEachChunk(systemIndex, collector); + } + + private static void bindChunk(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsSpaceCompatibilityIndexResource compatibility, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull PhysicsStepMode stepMode, + @Nonnull ArchetypeChunk chunk) { + for (int index = 0; index < chunk.size(); index++) { + SpaceComponent space = chunk.getComponent(index, SpaceComponent.getComponentType()); + if (space == null) { + continue; + } + UUID spaceUuid = PhysicsStoreSystemSupport.rowUuid(chunk, index); + if (PhysicsStoreSystemSupport.isNil(spaceUuid)) { + continue; + } + Ref spaceRef = chunk.getReferenceTo(index); + BackendSpaceHandle existingHandle = runtime.getSpaceHandle(spaceRef); + if (existingHandle != null) { + validateBoundSpaceBackend(runtime, restore, spaceRef, spaceUuid, space); + continue; + } + bindSpace(runtime, + compatibility, + restore, + stepMode, + spaceRef, + spaceUuid, + space, + chunk.getComponent(index, SolverSettingsComponent.getComponentType()), + chunk.getComponent(index, ExtensionSettingsComponent.getComponentType())); + } + } + + private static void validateBoundSpaceBackend(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull Ref spaceRef, + @Nonnull UUID spaceUuid, + @Nonnull SpaceComponent space) { + BackendId boundBackendId = runtime.getSpaceBackendId(spaceRef); + if (boundBackendId == null) { + restore.markFailed("PhysicsStore space " + spaceUuid + + " has a backend space handle without a backend id"); + return; + } + BackendId authoredBackendId = space.getBackendId(); + if (!boundBackendId.equals(authoredBackendId)) { + restore.markFailed("PhysicsStore space " + spaceUuid + + " changed backend id after binding: " + boundBackendId.value() + + " -> " + authoredBackendId.value()); + } + } + + private static void bindSpace(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsSpaceCompatibilityIndexResource compatibility, + @Nonnull PhysicsRestoreStatusResource restore, + @Nonnull PhysicsStepMode stepMode, + @Nonnull Ref ref, + @Nonnull UUID spaceUuid, + @Nonnull SpaceComponent space, + @Nullable SolverSettingsComponent solverSettings, + @Nullable ExtensionSettingsComponent extensionSettings) { + BackendId backendId = space.getBackendId(); + if (backendId.value().isBlank()) { + return; + } + PhysicsBackendRuntime backendRuntime = runtime.getRuntime(backendId); + if (backendRuntime == null) { + try { + backendRuntime = ImpulseBackendRegistry.createRuntime(backendId); + } catch (RuntimeException exception) { + restore.markFailed("PhysicsStore space " + spaceUuid + + " references unavailable backend id " + backendId.value()); + return; + } + runtime.putRuntime(backendId, backendRuntime); + } + SpaceId compatibilitySpaceId = compatibility.getSpaceId(spaceUuid); + if (compatibilitySpaceId == null) { + compatibilitySpaceId = SpaceId.next(); + } + SpaceId.reserveAtLeast(compatibilitySpaceId.value()); + BackendSpaceHandle handle = null; + try { + handle = new BackendSpaceHandle(backendRuntime.createSpace(compatibilitySpaceId)); + if (stepMode == PhysicsStepMode.CCD + && !backendRuntime.supportsContinuousCollision(handle.value())) { + throw new IllegalStateException("backend " + backendId.value() + + " does not support CCD step mode"); + } + Vector3f gravity = space.getGravity(); + backendRuntime.setGravity(handle.value(), gravity.x, gravity.y, gravity.z); + SpaceSettingsApplicationSystem.applyBackendSettings(backendRuntime, + handle, + solverSettings != null ? solverSettings : new SolverSettingsComponent(), + extensionSettings); + runtime.putSpaceHandle(ref, backendId, handle); + runtime.putSpaceMetadata(backendId, handle, spaceUuid, ref); + runtime.clearPendingSpaceSettings(ref); + compatibility.putSpace(compatibilitySpaceId, spaceUuid); + } catch (RuntimeException exception) { + if (handle != null) { + try { + backendRuntime.destroySpace(handle.value()); + } catch (RuntimeException ignored) { + // Preserve the original backend failure as the restore status. + } + } + restore.markFailed("PhysicsStore space " + spaceUuid + + " failed backend binding: " + exception.getMessage()); + } + } + + @Nonnull + @Override + public Query getQuery() { + return PhysicsStoreSystemSupport.uuidQuery(); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/binding/TargetBindingSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/binding/TargetBindingSystem.java new file mode 100644 index 00000000..9d0296eb --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/binding/TargetBindingSystem.java @@ -0,0 +1,174 @@ +package dev.hytalemodding.impulse.core.internal.systems.binding; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.QuerySystem; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource.PendingBodyOperation; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.systems.BodyCommandApplicationSystem; +import dev.hytalemodding.impulse.core.internal.systems.PhysicsStoreSystemSupport; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import java.util.Set; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +/** + * Applies copied kinematic target state to bound backend bodies before step submission. + */ +public final class TargetBindingSystem extends TickingSystem + implements QuerySystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, BodyCommandApplicationSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + BiConsumer, CommandBuffer> collector = + (chunk, _) -> applyTargets(runtime, chunk); + store.forEachChunk(systemIndex, collector); + applyPendingBodyOperations(runtime, restore); + } + + private static void applyTargets(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull ArchetypeChunk chunk) { + for (int index = 0; index < chunk.size(); index++) { + TargetComponent target = chunk.getComponent(index, TargetComponent.getComponentType()); + if (target == null || !target.isActive()) { + continue; + } + Ref ref = chunk.getReferenceTo(index); + BackendBodyHandle bodyHandle = runtime.getBodyHandle(ref); + BackendSpaceHandle spaceHandle = runtime.getBodySpaceHandle(ref); + if (bodyHandle == null || spaceHandle == null) { + continue; + } + PhysicsBackendRuntime backendRuntime = runtime.runtimeForBodyRef(ref); + if (backendRuntime == null) { + continue; + } + if (target.isTransformEnabled()) { + Vector3f position = target.getPosition(); + Quaternionf rotation = target.getRotation(); + backendRuntime.setBodyTransform(spaceHandle.value(), + bodyHandle.value(), + position.x, + position.y, + position.z, + rotation.x, + rotation.y, + rotation.z, + rotation.w); + } + if (target.isVelocityEnabled()) { + Vector3f linearVelocity = target.getLinearVelocity(); + Vector3f angularVelocity = target.getAngularVelocity(); + backendRuntime.setBodyVelocity(spaceHandle.value(), + bodyHandle.value(), + linearVelocity.x, + linearVelocity.y, + linearVelocity.z, + angularVelocity.x, + angularVelocity.y, + angularVelocity.z); + } + if (target.isActivate()) { + backendRuntime.activateBody(spaceHandle.value(), bodyHandle.value()); + } + } + } + + private static void applyPendingBodyOperations(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsRestoreStatusResource restore) { + for (PendingBodyOperation operation : runtime.drainPendingBodyOperations()) { + BackendSpaceHandle spaceHandle = runtime.getBodySpaceHandle(operation.bodyRef()); + BackendBodyHandle bodyHandle = runtime.getBodyHandle(operation.bodyRef()); + if (spaceHandle == null || bodyHandle == null) { + restore.recordSoftSkip("Pending body operation body is unbound: " + + operation.bodyUuid()); + continue; + } + PhysicsBackendRuntime backendRuntime = runtime.runtimeForBodyRef(operation.bodyRef()); + if (backendRuntime == null) { + restore.recordSoftSkip("Pending body operation backend runtime is missing: " + + operation.bodyUuid()); + continue; + } + int spaceId = spaceHandle.value(); + long bodyId = bodyHandle.value(); + switch (operation.kind()) { + case WAKE -> backendRuntime.activateBody(spaceId, bodyId); + case SLEEP -> backendRuntime.sleepBody(spaceId, bodyId); + case IMPULSE -> applyImpulse(backendRuntime, spaceId, bodyId, operation, false); + case TORQUE_IMPULSE -> applyImpulse(backendRuntime, spaceId, bodyId, operation, true); + case FORCE -> applyForce(backendRuntime, spaceId, bodyId, operation, false); + case TORQUE -> applyForce(backendRuntime, spaceId, bodyId, operation, true); + } + } + } + + private static void applyImpulse(@Nonnull PhysicsBackendRuntime backendRuntime, + int spaceId, + long bodyId, + @Nonnull PendingBodyOperation operation, + boolean torque) { + backendRuntime.applyBodyImpulse(spaceId, + bodyId, + operation.x(), + operation.y(), + operation.z(), + operation.hasOffset(), + operation.offsetX(), + operation.offsetY(), + operation.offsetZ(), + torque); + backendRuntime.activateBody(spaceId, bodyId); + } + + private static void applyForce(@Nonnull PhysicsBackendRuntime backendRuntime, + int spaceId, + long bodyId, + @Nonnull PendingBodyOperation operation, + boolean torque) { + backendRuntime.applyBodyForce(spaceId, + bodyId, + operation.x(), + operation.y(), + operation.z(), + operation.hasOffset(), + operation.offsetX(), + operation.offsetY(), + operation.offsetZ(), + torque); + backendRuntime.activateBody(spaceId, bodyId); + } + + @Nonnull + @Override + public Query getQuery() { + return PhysicsStoreSystemSupport.uuidQuery(); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/PhysicsBodyIdentityCleanupSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/PhysicsBodyIdentityCleanupSystem.java deleted file mode 100644 index 50830543..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/PhysicsBodyIdentityCleanupSystem.java +++ /dev/null @@ -1,89 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.body; - -import com.hypixel.hytale.component.CommandBuffer; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.ResourceType; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.query.Query; -import com.hypixel.hytale.component.system.RefChangeSystem; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyIdentityComponent; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import java.util.Objects; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Destroys entity-authored backend bodies when their durable ECS identity leaves the entity. - */ -public final class PhysicsBodyIdentityCleanupSystem - extends RefChangeSystem { - - @Nonnull - private final ComponentType identityType; - @Nonnull - private final ResourceType physicsWorldType; - @Nonnull - private final Query query; - - public PhysicsBodyIdentityCleanupSystem() { - this(PhysicsBodyIdentityComponent.getComponentType(), PhysicsWorldResource.getResourceType()); - } - - PhysicsBodyIdentityCleanupSystem( - @Nonnull ComponentType identityType, - @Nonnull ResourceType physicsWorldType) { - this.identityType = Objects.requireNonNull(identityType, "identityType"); - this.physicsWorldType = Objects.requireNonNull(physicsWorldType, "physicsWorldType"); - this.query = identityType; - } - - @Override - public void onComponentAdded(@Nonnull Ref ref, - @Nonnull PhysicsBodyIdentityComponent component, - @Nonnull Store store, - @Nonnull CommandBuffer commandBuffer) { - } - - @Override - public void onComponentSet(@Nonnull Ref ref, - @Nullable PhysicsBodyIdentityComponent oldComponent, - @Nonnull PhysicsBodyIdentityComponent newComponent, - @Nonnull Store store, - @Nonnull CommandBuffer commandBuffer) { - if (oldComponent != null && !sameIdentity(oldComponent, newComponent)) { - PhysicsWorldRuntimeResource.require(store.getResource(physicsWorldType)) - .destroyBody(oldComponent.getBodyKey()); - } - } - - @Override - public void onComponentRemoved(@Nonnull Ref ref, - @Nonnull PhysicsBodyIdentityComponent component, - @Nonnull Store store, - @Nonnull CommandBuffer commandBuffer) { - PhysicsWorldRuntimeResource.require(store.getResource(physicsWorldType)) - .destroyBody(component.getBodyKey()); - } - - @Nonnull - @Override - public ComponentType componentType() { - return identityType; - } - - @Nonnull - @Override - public Query getQuery() { - return query; - } - - private static boolean sameIdentity(@Nonnull PhysicsBodyIdentityComponent first, - @Nonnull PhysicsBodyIdentityComponent second) { - return first.getBodyKey().equals(second.getBodyKey()) - && Objects.equals(first.getSpaceId(), second.getSpaceId()) - && first.getPersistenceMode() == second.getPersistenceMode(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyCommandBatch.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyCommandBatch.java deleted file mode 100644 index 9fb8f368..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyCommandBatch.java +++ /dev/null @@ -1,148 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.body; - -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyKinematicTargetComponent; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandHandle; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.PhysicsCommandRecorder; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -import it.unimi.dsi.fastutil.objects.ObjectSet; -import java.util.ArrayList; -import java.util.List; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -/** - * Collects one entity-store tick of rigid-body ECS work into a single command submission. - */ -final class RigidBodyCommandBatch { - - @Nonnull - private final List spawns = new ArrayList<>(); - @Nonnull - private final List kinematicTargets = new ArrayList<>(); - @Nonnull - private final ObjectSet pendingBodyKeys = new ObjectOpenHashSet<>(); - - void addSpawn(@Nonnull RigidBodySpawnPlan plan, @Nonnull Vector3f position) { - spawns.add(new SpawnCommand(plan, position)); - pendingBodyKeys.add(plan.bodyKey()); - } - - void addKinematicTarget(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodyKinematicTargetComponent target) { - kinematicTargets.add(new KinematicTargetCommand(bodyKey, target)); - } - - boolean hasPendingBody(@Nonnull RigidBodyKey bodyKey) { - return pendingBodyKeys.contains(bodyKey); - } - - boolean hasKinematicTargets() { - return !kinematicTargets.isEmpty(); - } - - @Nonnull - List kinematicTargetKeys() { - List keys = new ArrayList<>(kinematicTargets.size()); - for (KinematicTargetCommand target : kinematicTargets) { - keys.add(target.bodyKey()); - } - return keys; - } - - int expectedOperations() { - int operations = spawns.size(); - for (KinematicTargetCommand target : kinematicTargets) { - operations += target.operationCount(); - } - return operations; - } - - @Nullable - PhysicsCommandHandle submit(@Nonnull PhysicsWorldRuntimeResource resource) { - int expectedOperations = expectedOperations(); - if (expectedOperations == 0) { - return null; - } - return resource.submitCommands(0L, expectedOperations, this::record); - } - - void record(@Nonnull PhysicsCommandRecorder commands) { - for (SpawnCommand spawn : spawns) { - RigidBodySpawnPlan plan = spawn.plan(); - commands.spawnBody(plan.bodyKey(), recorder -> recorder - .space(plan.spaceId()) - .shape(plan.requireShape()) - .mass(plan.mass()) - .type(plan.bodyType()) - .position(spawn.position()) - .settings(plan.settings()) - .kind(PhysicsBodyKind.BODY) - .persistence(plan.persistenceMode())); - } - for (KinematicTargetCommand target : kinematicTargets) { - target.record(commands); - } - } - - private record SpawnCommand(@Nonnull RigidBodySpawnPlan plan, - @Nonnull Vector3f position) { - - private SpawnCommand { - position = new Vector3f(position); - } - } - - private record KinematicTargetCommand(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f position, - @Nonnull Quaternionf rotation, - @Nonnull Vector3f linearVelocity, - @Nonnull Vector3f angularVelocity, - boolean transformEnabled, - boolean velocityEnabled, - boolean activate) { - - private KinematicTargetCommand(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodyKinematicTargetComponent target) { - this(bodyKey, - target.getPosition(), - target.getRotation(), - target.getLinearVelocity(), - target.getAngularVelocity(), - target.isTransformEnabled(), - target.isVelocityEnabled(), - target.isActivate()); - } - - private KinematicTargetCommand { - position = new Vector3f(position); - rotation = new Quaternionf(rotation); - linearVelocity = new Vector3f(linearVelocity); - angularVelocity = new Vector3f(angularVelocity); - } - - int operationCount() { - int count = 0; - if (transformEnabled) { - count++; - } - if (velocityEnabled) { - count++; - } - return count; - } - - void record(@Nonnull PhysicsCommandRecorder commands) { - if (transformEnabled) { - commands.setBodyTransform(bodyKey, position, rotation, activate); - } - if (velocityEnabled) { - commands.setBodyVelocity(bodyKey, linearVelocity, angularVelocity, activate); - } - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyKinematicTargetState.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyKinematicTargetState.java deleted file mode 100644 index ac83df01..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyKinematicTargetState.java +++ /dev/null @@ -1,108 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.body; - -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyKinematicTargetComponent; -import it.unimi.dsi.fastutil.objects.Object2ObjectMap; -import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; -import java.util.Collection; -import javax.annotation.Nonnull; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -/** - * Tracks the last kinematic target submitted from ECS so unchanged targets do not resubmit. - */ -final class RigidBodyKinematicTargetState { - - @Nonnull - private final Object2ObjectMap submittedTargets = - new Object2ObjectOpenHashMap<>(); - private long generation; - - synchronized void beginTick() { - generation++; - } - - synchronized boolean shouldSubmit(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodyKinematicTargetComponent target) { - TargetSnapshot snapshot = TargetSnapshot.copyOf(target); - if (!snapshot.hasOperations()) { - submittedTargets.remove(bodyKey); - return false; - } - TargetEntry previous = submittedTargets.get(bodyKey); - if (previous != null && snapshot.equals(previous.snapshot())) { - submittedTargets.put(bodyKey, new TargetEntry(snapshot, generation)); - return false; - } - submittedTargets.put(bodyKey, new TargetEntry(snapshot, generation)); - return true; - } - - synchronized void finishTick() { - submittedTargets.values().removeIf(entry -> entry.generation() != generation); - } - - synchronized void clear(@Nonnull RigidBodyKey bodyKey) { - submittedTargets.remove(bodyKey); - } - - synchronized void clearAll(@Nonnull Collection bodyKeys) { - for (RigidBodyKey bodyKey : bodyKeys) { - submittedTargets.remove(bodyKey); - } - } - - synchronized int trackedTargetCount() { - return submittedTargets.size(); - } - - private record TargetEntry(@Nonnull TargetSnapshot snapshot, long generation) { - } - - private record TargetSnapshot(float positionX, - float positionY, - float positionZ, - float rotationX, - float rotationY, - float rotationZ, - float rotationW, - float linearVelocityX, - float linearVelocityY, - float linearVelocityZ, - float angularVelocityX, - float angularVelocityY, - float angularVelocityZ, - boolean transformEnabled, - boolean velocityEnabled, - boolean activate) { - - @Nonnull - static TargetSnapshot copyOf(@Nonnull PhysicsBodyKinematicTargetComponent target) { - Vector3f position = target.getPosition(); - Quaternionf rotation = target.getRotation(); - Vector3f linearVelocity = target.getLinearVelocity(); - Vector3f angularVelocity = target.getAngularVelocity(); - return new TargetSnapshot(position.x, - position.y, - position.z, - rotation.x, - rotation.y, - rotation.z, - rotation.w, - linearVelocity.x, - linearVelocity.y, - linearVelocity.z, - angularVelocity.x, - angularVelocity.y, - angularVelocity.z, - target.isTransformEnabled(), - target.isVelocityEnabled(), - target.isActivate()); - } - - boolean hasOperations() { - return transformEnabled || velocityEnabled; - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyLifecycleCleanupSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyLifecycleCleanupSystem.java deleted file mode 100644 index 2c22c951..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyLifecycleCleanupSystem.java +++ /dev/null @@ -1,87 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.body; - -import com.hypixel.hytale.component.CommandBuffer; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.query.Query; -import com.hypixel.hytale.component.system.RefChangeSystem; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyLifecycleComponent; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Applies explicit backend destruction when ECS ownership is removed. - */ -public class RigidBodyLifecycleCleanupSystem - extends RefChangeSystem { - - private static final ComponentType LIFECYCLE_TYPE = - PhysicsBodyLifecycleComponent.getComponentType(); - private static final Query QUERY = LIFECYCLE_TYPE; - - @Override - public void onComponentAdded(@Nonnull Ref ref, - @Nonnull PhysicsBodyLifecycleComponent component, - @Nonnull Store store, - @Nonnull CommandBuffer commandBuffer) { - } - - @Override - public void onComponentSet(@Nonnull Ref ref, - @Nullable PhysicsBodyLifecycleComponent oldComponent, - @Nonnull PhysicsBodyLifecycleComponent newComponent, - @Nonnull Store store, - @Nonnull CommandBuffer commandBuffer) { - if (oldComponent != null - && shouldDestroy(oldComponent) - && !sameBody(oldComponent, newComponent)) { - destroy(store, oldComponent); - } - } - - @Override - public void onComponentRemoved(@Nonnull Ref ref, - @Nonnull PhysicsBodyLifecycleComponent component, - @Nonnull Store store, - @Nonnull CommandBuffer commandBuffer) { - if (shouldDestroy(component)) { - destroy(store, component); - } - } - - @Nonnull - @Override - public ComponentType componentType() { - return LIFECYCLE_TYPE; - } - - @Nonnull - @Override - public Query getQuery() { - return QUERY; - } - - private static boolean shouldDestroy(@Nonnull PhysicsBodyLifecycleComponent component) { - return component.getBodyKey() != null - && component.getState() != PhysicsBodyLifecycleComponent.State.FAILED - && component.getState() != PhysicsBodyLifecycleComponent.State.DESTROYED; - } - - private static boolean sameBody(@Nonnull PhysicsBodyLifecycleComponent first, - @Nonnull PhysicsBodyLifecycleComponent second) { - RigidBodyKey firstKey = first.getBodyKey(); - return firstKey != null && firstKey.equals(second.getBodyKey()); - } - - private static void destroy(@Nonnull Store store, - @Nonnull PhysicsBodyLifecycleComponent component) { - RigidBodyKey bodyKey = component.getBodyKey(); - if (bodyKey != null) { - PhysicsWorldRuntimeResource.require(store).destroyBody(bodyKey); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyReconciliationPolicy.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyReconciliationPolicy.java deleted file mode 100644 index 0a4d5dab..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyReconciliationPolicy.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.body; - -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyLifecycleComponent; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -final class RigidBodyReconciliationPolicy { - - private RigidBodyReconciliationPolicy() { - } - - static boolean shouldSuppressBodyReconciliation(@Nullable PhysicsBodyLifecycleComponent lifecycle, - @Nullable PersistentPhysicsWorldResource persistent, - @Nonnull RigidBodyKey bodyKey) { - if (lifecycle != null - && lifecycle.getState() == PhysicsBodyLifecycleComponent.State.DESTROYED) { - return true; - } - return persistent != null && persistent.isRuntimeBodySkipped(bodyKey); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyReconciliationSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyReconciliationSystem.java deleted file mode 100644 index aa6c150d..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyReconciliationSystem.java +++ /dev/null @@ -1,331 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.body; - -import com.hypixel.hytale.component.ArchetypeChunk; -import com.hypixel.hytale.component.CommandBuffer; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.dependency.Dependency; -import com.hypixel.hytale.component.dependency.Order; -import com.hypixel.hytale.component.dependency.SystemDependency; -import com.hypixel.hytale.component.dependency.SystemGroupDependency; -import com.hypixel.hytale.component.query.Query; -import com.hypixel.hytale.component.system.QuerySystem; -import com.hypixel.hytale.component.system.tick.TickingSystem; -import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.systems.sync.PhysicsSyncSystem; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyCollisionComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyDynamicsComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyIdentityComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyKinematicTargetComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyLifecycleComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyMaterialComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyShapeComponent; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandHandle; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.function.BiConsumer; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3d; -import org.joml.Vector3f; - -/** - * Reconciles ECS rigid-body components into existing physics commands. - */ -public class RigidBodyReconciliationSystem extends TickingSystem - implements QuerySystem { - - private static final ComponentType IDENTITY_TYPE = - PhysicsBodyIdentityComponent.getComponentType(); - private static final ComponentType SHAPE_TYPE = - PhysicsBodyShapeComponent.getComponentType(); - private static final ComponentType DYNAMICS_TYPE = - PhysicsBodyDynamicsComponent.getComponentType(); - private static final ComponentType MATERIAL_TYPE = - PhysicsBodyMaterialComponent.getComponentType(); - private static final ComponentType COLLISION_TYPE = - PhysicsBodyCollisionComponent.getComponentType(); - private static final ComponentType KINEMATIC_TARGET_TYPE = - PhysicsBodyKinematicTargetComponent.getComponentType(); - private static final ComponentType LIFECYCLE_TYPE = - PhysicsBodyLifecycleComponent.getComponentType(); - private static final ComponentType ATTACHMENT_TYPE = - PhysicsBodyAttachmentComponent.getComponentType(); - private static final ComponentType TRANSFORM_TYPE = - TransformComponent.getComponentType(); - - private static final Query QUERY = IDENTITY_TYPE; - - private final Set> dependencies = Set.of( - new SystemGroupDependency<>(Order.AFTER, ImpulsePlugin.get().getPersistenceRestoreGroup()), - new SystemDependency<>(Order.BEFORE, PhysicsSyncSystem.class) - ); - @Nonnull - private final Map, RigidBodyKinematicTargetState> kinematicStatesByStore = - Collections.synchronizedMap(new WeakHashMap<>()); - - @Override - public void tick(float dt, int systemIndex, @Nonnull Store store) { - PhysicsWorldRuntimeResource resource = PhysicsWorldRuntimeResource.require(store); - RigidBodyCommandBatch physicsBatch = new RigidBodyCommandBatch(); - RigidBodyKinematicTargetState kinematicState = kinematicStateFor(store); - PersistentPhysicsWorldResource persistent = store.getResource( - PersistentPhysicsWorldResource.getResourceType()); - - kinematicState.beginTick(); - try { - BiConsumer, CommandBuffer> collector = - (chunk, commandBuffer) -> reconcileChunk(chunk, - commandBuffer, - resource, - persistent, - physicsBatch, - kinematicState); - store.forEachChunk(systemIndex, collector); - } finally { - kinematicState.finishTick(); - } - - submitBatch(physicsBatch, resource, kinematicState); - } - - private static void reconcileChunk(@Nonnull ArchetypeChunk chunk, - @Nonnull CommandBuffer commandBuffer, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nullable PersistentPhysicsWorldResource persistent, - @Nonnull RigidBodyCommandBatch physicsBatch, - @Nonnull RigidBodyKinematicTargetState kinematicState) { - for (int index = 0; index < chunk.size(); index++) { - reconcileEntity(index, - chunk, - commandBuffer, - resource, - persistent, - physicsBatch, - kinematicState); - } - } - - private static void reconcileEntity( - int index, - @Nonnull ArchetypeChunk chunk, - @Nonnull CommandBuffer commandBuffer, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nullable PersistentPhysicsWorldResource persistent, - @Nonnull RigidBodyCommandBatch physicsBatch, - @Nonnull RigidBodyKinematicTargetState kinematicState) { - PhysicsBodyIdentityComponent identity = chunk.getComponent(index, IDENTITY_TYPE); - if (identity == null) { - return; - } - - Ref ref = chunk.getReferenceTo(index); - PhysicsBodyLifecycleComponent lifecycle = chunk.getComponent(index, LIFECYCLE_TYPE); - RigidBodyKey bodyKey = identity.getBodyKey(); - if (RigidBodyReconciliationPolicy.shouldSuppressBodyReconciliation(lifecycle, persistent, bodyKey)) { - kinematicState.clear(bodyKey); - clearAttachment(ref, chunk.getComponent(index, ATTACHMENT_TYPE), commandBuffer); - if (lifecycle == null || lifecycle.getState() != PhysicsBodyLifecycleComponent.State.DESTROYED) { - commandBuffer.putComponent(ref, - LIFECYCLE_TYPE, - PhysicsBodyLifecycleComponent.failed(bodyKey, - "Persisted body restore skipped")); - } - return; - } - - boolean registeredOrPending = resource.hasPublishedOrPendingBodyRegistration(bodyKey) - || physicsBatch.hasPendingBody(bodyKey); - - if (registeredOrPending) { - reconcileExistingBody(ref, - chunk, - index, - commandBuffer, - resource, - physicsBatch, - kinematicState, - identity, - bodyKey, - lifecycle); - return; - } - - RigidBodySpawnPlan plan; - try { - plan = createPlan(index, chunk, identity); - } catch (IllegalArgumentException exception) { - kinematicState.clear(bodyKey); - commandBuffer.putComponent(ref, - LIFECYCLE_TYPE, - PhysicsBodyLifecycleComponent.failed(bodyKey, exception.getMessage())); - clearAttachment(ref, chunk.getComponent(index, ATTACHMENT_TYPE), commandBuffer); - return; - } - - physicsBatch.addSpawn(plan, spawnPosition(chunk, index)); - commandBuffer.putComponent(ref, - LIFECYCLE_TYPE, - PhysicsBodyLifecycleComponent.pending(plan.bodyKey())); - reconcileAttachment(ref, - plan, - chunk.getComponent(index, ATTACHMENT_TYPE), - commandBuffer); - } - - private static void reconcileExistingBody(@Nonnull Ref ref, - @Nonnull ArchetypeChunk chunk, - int index, - @Nonnull CommandBuffer commandBuffer, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull RigidBodyCommandBatch physicsBatch, - @Nonnull RigidBodyKinematicTargetState kinematicState, - @Nonnull PhysicsBodyIdentityComponent identity, - @Nonnull RigidBodyKey bodyKey, - @Nullable PhysicsBodyLifecycleComponent lifecycle) { - RigidBodySpawnPlan plan; - try { - plan = createPlan(index, chunk, identity); - } catch (IllegalArgumentException exception) { - commandBuffer.putComponent(ref, - LIFECYCLE_TYPE, - PhysicsBodyLifecycleComponent.failed(bodyKey, exception.getMessage())); - kinematicState.clear(bodyKey); - clearAttachment(ref, chunk.getComponent(index, ATTACHMENT_TYPE), commandBuffer); - return; - } - PhysicsBodyLifecycleComponent.State state = resource.getBodyRegistrationView(bodyKey) != null - ? PhysicsBodyLifecycleComponent.State.CREATED - : PhysicsBodyLifecycleComponent.State.PENDING; - if (lifecycle == null - || lifecycle.getState() != state - || !bodyKey.equals(lifecycle.getBodyKey())) { - PhysicsBodyLifecycleComponent updated = state == PhysicsBodyLifecycleComponent.State.CREATED - ? PhysicsBodyLifecycleComponent.created(bodyKey) - : PhysicsBodyLifecycleComponent.pending(bodyKey); - commandBuffer.putComponent(ref, LIFECYCLE_TYPE, updated); - } - reconcileAttachment(ref, - plan, - chunk.getComponent(index, ATTACHMENT_TYPE), - commandBuffer); - reconcileKinematicTarget(physicsBatch, - kinematicState, - bodyKey, - chunk.getComponent(index, KINEMATIC_TARGET_TYPE)); - } - - @Nonnull - private static RigidBodySpawnPlan createPlan(int index, - @Nonnull ArchetypeChunk chunk, - @Nonnull PhysicsBodyIdentityComponent identity) { - return RigidBodySpawnPlan.create(identity, - chunk.getComponent(index, SHAPE_TYPE), - chunk.getComponent(index, DYNAMICS_TYPE), - chunk.getComponent(index, MATERIAL_TYPE), - chunk.getComponent(index, COLLISION_TYPE)); - } - - @Nonnull - private static Vector3f spawnPosition(@Nonnull ArchetypeChunk chunk, int index) { - PhysicsBodyKinematicTargetComponent target = chunk.getComponent(index, KINEMATIC_TARGET_TYPE); - if (target != null && target.isTransformEnabled()) { - return new Vector3f(target.getPosition()); - } - TransformComponent transform = chunk.getComponent(index, TRANSFORM_TYPE); - if (transform == null) { - return new Vector3f(); - } - Vector3d position = transform.getPosition(); - return new Vector3f((float) position.x, (float) position.y, (float) position.z); - } - - private static void reconcileAttachment(@Nonnull Ref ref, - @Nonnull RigidBodySpawnPlan plan, - @Nullable PhysicsBodyAttachmentComponent current, - @Nonnull CommandBuffer commandBuffer) { - if (current != null - && current.getBodyKey().equals(plan.bodyKey()) - && plan.spaceId().equals(current.getSpaceId()) - && current.getLifecycle() - == PhysicsBodyAttachmentComponent.AttachmentLifecycle.EXTERNAL_ENTITY) { - return; - } - commandBuffer.putComponent(ref, - ATTACHMENT_TYPE, - PhysicsBodyAttachmentComponent.externalEntity(plan.bodyKey(), plan.spaceId())); - } - - private static void clearAttachment(@Nonnull Ref ref, - @Nullable PhysicsBodyAttachmentComponent current, - @Nonnull CommandBuffer commandBuffer) { - if (current != null) { - commandBuffer.removeComponent(ref, ATTACHMENT_TYPE); - } - } - - private static void reconcileKinematicTarget(@Nonnull RigidBodyCommandBatch physicsBatch, - @Nonnull RigidBodyKinematicTargetState kinematicState, - @Nonnull RigidBodyKey bodyKey, - @Nullable PhysicsBodyKinematicTargetComponent target) { - if (target == null || (!target.isTransformEnabled() && !target.isVelocityEnabled())) { - kinematicState.clear(bodyKey); - return; - } - if (kinematicState.shouldSubmit(bodyKey, target)) { - physicsBatch.addKinematicTarget(bodyKey, target); - } - } - - private static void submitBatch(@Nonnull RigidBodyCommandBatch physicsBatch, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull RigidBodyKinematicTargetState kinematicState) { - boolean hasKinematicTargets = physicsBatch.hasKinematicTargets(); - List kinematicTargetKeys = hasKinematicTargets - ? physicsBatch.kinematicTargetKeys() - : List.of(); - PhysicsCommandHandle handle = physicsBatch.submit(resource); - if (handle == null || !hasKinematicTargets) { - return; - } - handle.completionSummary().whenComplete((completion, failure) -> { - if (failure != null || !completion.allApplied()) { - kinematicState.clearAll(kinematicTargetKeys); - } - }); - } - - @Nonnull - private RigidBodyKinematicTargetState kinematicStateFor(@Nonnull Store store) { - synchronized (kinematicStatesByStore) { - RigidBodyKinematicTargetState state = kinematicStatesByStore.get(store); - if (state == null) { - state = new RigidBodyKinematicTargetState(); - kinematicStatesByStore.put(store, state); - } - return state; - } - } - - @Nonnull - @Override - public Query getQuery() { - return QUERY; - } - - @Nonnull - @Override - public Set> getDependencies() { - return dependencies; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodySpawnPlan.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodySpawnPlan.java deleted file mode 100644 index 70d13767..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodySpawnPlan.java +++ /dev/null @@ -1,76 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.body; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyCollisionComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyComponentValues; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyDynamicsComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyIdentityComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyMaterialComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyShapeComponent; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import java.util.Objects; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -record RigidBodySpawnPlan(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - @Nonnull PhysicsBodyType bodyType, - float mass, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - - private static final String MISSING_SPACE = - "PhysicsBodyIdentityComponent must hold a positive explicit SpaceId"; - - RigidBodySpawnPlan { - Objects.requireNonNull(bodyKey, "bodyKey"); - Objects.requireNonNull(spaceId, "spaceId"); - Objects.requireNonNull(shape, "shape"); - Objects.requireNonNull(bodyType, "bodyType"); - Objects.requireNonNull(settings, "settings"); - Objects.requireNonNull(persistenceMode, "persistenceMode"); - } - - @Nonnull - static RigidBodySpawnPlan create(@Nonnull PhysicsBodyIdentityComponent identity, - @Nullable PhysicsBodyShapeComponent shape, - @Nullable PhysicsBodyDynamicsComponent dynamics, - @Nullable PhysicsBodyMaterialComponent material, - @Nullable PhysicsBodyCollisionComponent collision) { - Objects.requireNonNull(identity, "identity"); - if (!PhysicsBodyComponentValues.hasExplicitSpace(identity)) { - throw new IllegalArgumentException(MISSING_SPACE); - } - if (shape == null) { - throw new IllegalArgumentException("PhysicsBodyShapeComponent is required"); - } - if (dynamics == null) { - throw new IllegalArgumentException("PhysicsBodyDynamicsComponent is required"); - } - if (material == null) { - throw new IllegalArgumentException("PhysicsBodyMaterialComponent is required"); - } - if (collision == null) { - throw new IllegalArgumentException("PhysicsBodyCollisionComponent is required"); - } - assert identity.getSpaceId() != null; - return new RigidBodySpawnPlan( - identity.getBodyKey(), - identity.getSpaceId(), - PhysicsBodyComponentValues.toShapeSpec(shape), - dynamics.getBodyType(), - dynamics.getMass(), - PhysicsBodyComponentValues.toSpawnSettings(dynamics, material, collision), - identity.getPersistenceMode()); - } - - @Nonnull - PhysicsShapeSpec requireShape() { - return shape; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsChunkDebugSectionView.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsChunkDebugSectionView.java new file mode 100644 index 00000000..0ae7acf0 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsChunkDebugSectionView.java @@ -0,0 +1,21 @@ +package dev.hytalemodding.impulse.core.internal.systems.debug; + +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.SectionCollisionGeometry.BoxCollider; +import java.util.List; +import javax.annotation.Nonnull; + +/** + * Copied terrain debug section used by the internal debug renderer. + */ +public record PhysicsChunkDebugSectionView(int chunkX, + int sectionY, + int chunkZ, + boolean voxelTerrain, + @Nonnull List fullCubeBoxes, + @Nonnull List detailBoxes) { + + public PhysicsChunkDebugSectionView { + fullCubeBoxes = List.copyOf(fullCubeBoxes); + detailBoxes = List.copyOf(detailBoxes); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/view/PhysicsDebugContactView.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugContactView.java similarity index 87% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/view/PhysicsDebugContactView.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugContactView.java index 25dbba70..33da8e21 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/view/PhysicsDebugContactView.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugContactView.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.simulation.view; +package dev.hytalemodding.impulse.core.internal.systems.debug; /** * Copied contact point used by the internal debug renderer. diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/view/PhysicsDebugJointView.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugJointView.java similarity index 90% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/view/PhysicsDebugJointView.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugJointView.java index 0c39db7c..d65b941d 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/simulation/view/PhysicsDebugJointView.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugJointView.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.simulation.view; +package dev.hytalemodding.impulse.core.internal.systems.debug; /** * Copied joint debug shape used by the internal debug renderer. diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugRenderer.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugRenderer.java index 9ac69dec..3e8770ee 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugRenderer.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugRenderer.java @@ -7,10 +7,8 @@ import dev.hytalemodding.impulse.api.PhysicsAxis; import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; import dev.hytalemodding.impulse.core.internal.math.PhysicsVisualPoseMath; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.SectionCollisionGeometry.BoxCollider; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsDebugContactView; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsDebugJointView; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.SectionCollisionGeometry.BoxCollider; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; import java.util.Collection; import javax.annotation.Nonnull; import org.joml.Matrix4d; @@ -22,7 +20,7 @@ final class PhysicsDebugRenderer { private static final double SHAPE_INFLATION = 1.025; - private static final double WORLD_COLLISION_EDGE_PADDING = 0.0125; + private static final double TERRAIN_EDGE_PADDING = 0.0125; private static final float MIN_DEBUG_LIFETIME = 0.08f; private static final double MIN_ARROW_LENGTH = 0.05; private static final double MAX_ARROW_LENGTH = 4.0; @@ -230,7 +228,7 @@ static void renderRay(@Nonnull Collection viewers, renderArrow(viewers, start, direction, color, time); } - static void renderWorldCollisionSection(@Nonnull Collection viewers, + static void renderPhysicsChunkCollisionSection(@Nonnull Collection viewers, int chunkX, int sectionY, int chunkZ, @@ -245,10 +243,10 @@ static void renderWorldCollisionSection(@Nonnull Collection viewers, new Vector3d(halfSection, halfSection, halfSection), color, time, - WORLD_COLLISION_EDGE_PADDING); + TERRAIN_EDGE_PADDING); } - static void renderWorldCollisionBox(@Nonnull Collection viewers, + static void renderPhysicsChunkCollisionBox(@Nonnull Collection viewers, @Nonnull BoxCollider box, @Nonnull Vector3f color, float time) { @@ -257,12 +255,12 @@ static void renderWorldCollisionBox(@Nonnull Collection viewers, new Vector3d(box.halfX(), box.halfY(), box.halfZ()), color, time, - WORLD_COLLISION_EDGE_PADDING); + TERRAIN_EDGE_PADDING); } static Vector3d centerFromSyncedTransform(@Nonnull PhysicsBodySnapshot snapshot, @Nonnull Vector3d transformPosition, - @Nonnull PhysicsBodyAttachmentComponent attachment, + @Nonnull BodyAttachmentComponent attachment, @Nonnull Quaterniond bodyRotation) { return PhysicsVisualPoseMath.bodyCenterFromVisualPose(transformPosition, bodyRotation, @@ -275,7 +273,7 @@ static Vector3d centerFromSyncedTransform(@Nonnull PhysicsBodySnapshot snapshot, static BodyDebugPose bodyPoseFromSyncedTransform(@Nonnull PhysicsBodySnapshot snapshot, @Nonnull Vector3d transformPosition, @Nonnull Quaterniond transformRotation, - @Nonnull PhysicsBodyAttachmentComponent attachment) { + @Nonnull BodyAttachmentComponent attachment) { Quaterniond bodyRotation = new Quaterniond(transformRotation); if (!isIdentity(attachment.getLocalRotationOffset())) { bodyRotation.mul(new Quaterniond(attachment.getLocalRotationOffset()).invert()).normalize(); diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugSystem.java index 88d7ea83..b17bbfb5 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugSystem.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugSystem.java @@ -14,23 +14,31 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsAxis; import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; import dev.hytalemodding.impulse.api.ShapeType; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent.AttachmentLifecycle; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsEntityAttachments; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent.AttachmentLifecycle; import dev.hytalemodding.impulse.core.internal.resources.PhysicsDebugResource; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyRegistrationView; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.SectionCollisionGeometry.BoxCollider; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsDebugContactView; -import dev.hytalemodding.impulse.core.internal.simulation.query.PhysicsDebugContactsQuery; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsDebugJointView; -import dev.hytalemodding.impulse.core.internal.simulation.query.PhysicsDebugJointsQuery; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldVoxelCollisionCache.DebugSection; -import dev.hytalemodding.impulse.core.internal.systems.sync.PhysicsSyncSystem; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources.PhysicsDebugOverlayResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollision; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsSpaces; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.SectionCollisionGeometry.BoxCollider; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.sync.PhysicsSyncSystem; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; import java.util.ArrayList; @@ -47,6 +55,7 @@ import java.util.concurrent.CompletionStage; import java.util.function.Supplier; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.joml.Quaterniond; import org.joml.Vector3d; import org.joml.Vector3f; @@ -60,11 +69,10 @@ */ public class PhysicsDebugSystem extends TickingSystem { - private static final ComponentType ATTACHMENT_TYPE = - PhysicsBodyAttachmentComponent.getComponentType(); - private static final ComponentType TRANSFORM_TYPE = - TransformComponent.getComponentType(); - + @Nonnull + private final ComponentType attachmentType; + @Nonnull + private final ComponentType transformType; @Nonnull private final Map, DebugQueryCache> queryCachesByStore = Collections.synchronizedMap(new WeakHashMap<>()); @@ -73,6 +81,16 @@ public class PhysicsDebugSystem extends TickingSystem { new SystemDependency<>(Order.AFTER, UpdateLocationSystems.TickingSystem.class) ); + public PhysicsDebugSystem() { + this(BodyAttachmentComponent.getComponentType(), TransformComponent.getComponentType()); + } + + PhysicsDebugSystem(@Nonnull ComponentType attachmentType, + @Nonnull ComponentType transformType) { + this.attachmentType = Objects.requireNonNull(attachmentType, "attachmentType"); + this.transformType = Objects.requireNonNull(transformType, "transformType"); + } + @Override public Set> getDependencies() { return dependencies; @@ -81,38 +99,42 @@ public Set> getDependencies() { @Override public void tick(float dt, int index, @Nonnull Store store) { World world = store.getExternalData().getWorld(); - PhysicsWorldRuntimeResource resource = PhysicsWorldRuntimeResource.require(store); - PhysicsDebugResource debug = store.getResource(PhysicsDebugResource.getResourceType()); + assert PhysicsDebugOverlayResource.getResourceType() != null; + PhysicsDebugOverlayResource overlay = + store.getResource(PhysicsDebugOverlayResource.getResourceType()); - if (!debug.hasSubscribers()) { + if (!overlay.hasSubscribers()) { return; } - List viewers = resolveSubscribers(world, debug); + List viewers = resolveSubscribers(world, overlay); if (viewers.isEmpty()) { return; } - boolean overlayDue = debug.tickOverlayBudget(dt); - boolean worldCollisionDue = debug.tickWorldCollisionBudget(dt); - if (!overlayDue && !worldCollisionDue) { + boolean overlayDue = overlay.tickOverlayBudget(dt); + boolean terrainDue = overlay.tickPhysicsChunkBudget(dt); + if (!overlayDue && !terrainDue) { return; } + Store physicsStore = PhysicsThreading.store(world); + PhysicsDebugResource debug = + physicsStore.getResource(PhysicsDebugResource.getResourceType()); boolean debugShapes = debug.isDebugShapesEnabled(); boolean debugMotion = debug.isDebugMotionEnabled(); boolean debugContacts = debug.isDebugContactsEnabled(); boolean debugJoints = debug.isDebugJointsEnabled(); - boolean debugWorldCollision = debug.isDebugWorldCollisionEnabled(); + boolean debugCollision = debug.isDebugPhysicsChunkCollisionEnabled(); if (!debugShapes && !debugMotion && !debugContacts && !debugJoints - && !debugWorldCollision) { + && !debugCollision) { return; } float overlayLifetime = PhysicsDebugRenderer.lifetimeForRefresh( - debug.getOverlayRefreshSeconds(), dt); - float worldCollisionLifetime = PhysicsDebugRenderer.lifetimeForRefresh( - debug.getWorldCollisionRefreshSeconds(), dt); + overlay.getOverlayRefreshSeconds(), dt); + float terrainLifetime = PhysicsDebugRenderer.lifetimeForRefresh( + overlay.getPhysicsChunkRefreshSeconds(), dt); DebugQueryCache queryCache = queryCacheFor(store); for (PlayerRef viewer : viewers) { @@ -123,58 +145,61 @@ public void tick(float dt, int index, @Nonnull Store store) { if (overlayDue && (debugShapes || debugMotion)) { int renderedBodies = renderEntityBodies(target, store, - resource, + physicsStore, viewerPosition, - debug.getViewRadius(), + overlay.getViewRadius(), debugShapes, debugMotion, - debug.getMaxBodies(), + overlay.getMaxBodies(), overlayLifetime); renderDetachedBodies(target, - resource, + store, + physicsStore, viewerPosition, - debug.getViewRadius(), + overlay.getViewRadius(), debugShapes, debugMotion, - Math.max(0, debug.getMaxBodies() - renderedBodies), + Math.max(0, overlay.getMaxBodies() - renderedBodies), overlayLifetime); } - for (PhysicsSpaceBinding space : resource.getSpaceBindings()) { + for (SpaceId spaceId : PhysicsSpaces.spaceIds(physicsStore)) { if (overlayDue && debugShapes) { - renderSpaceOnlyShapes(target, resource, space, overlayLifetime); + renderSpaceOnlyShapes(target, physicsStore, spaceId, overlayLifetime); } if (overlayDue && debugContacts) { renderContacts(target, - resource, - space, + physicsStore, + spaceId, viewerUuid, queryCache, viewerPosition, - debug.getViewRadius(), - debug.getMaxContacts(), + overlay.getViewRadius(), + overlay.getMaxContacts(), overlayLifetime); } if (overlayDue && debugJoints) { renderJoints(target, - resource, - space, + physicsStore, + spaceId, viewerUuid, queryCache, viewerPosition, - debug.getViewRadius(), - debug.getMaxJoints(), + overlay.getViewRadius(), + overlay.getMaxJoints(), overlayLifetime); } - if (worldCollisionDue && debugWorldCollision) { - renderWorldCollision(target, - resource, - space, + if (terrainDue && debugCollision) { + renderPhysicsChunkCollision(target, + physicsStore, + spaceId, + viewerUuid, + queryCache, viewerPosition, - debug.getViewRadius(), - debug.getMaxWorldCollisionSections(), - debug.getMaxWorldCollisionBoxes(), - worldCollisionLifetime); + overlay.getViewRadius(), + overlay.getMaxPhysicsChunkSections(), + overlay.getMaxPhysicsChunkBoxes(), + terrainLifetime); } } } @@ -189,8 +214,8 @@ private DebugQueryCache queryCacheFor(@Nonnull Store store) { @Nonnull private static List resolveSubscribers(@Nonnull World world, - @Nonnull PhysicsDebugResource debug) { - Set active = new ObjectOpenHashSet<>(debug.getSubscriberUuids()); + @Nonnull PhysicsDebugOverlayResource overlay) { + Set active = new ObjectOpenHashSet<>(overlay.getSubscriberUuids()); List viewers = new ArrayList<>(); for (PlayerRef player : world.getPlayerRefs()) { if (active.remove(player.getUuid())) { @@ -199,14 +224,14 @@ private static List resolveSubscribers(@Nonnull World world, } for (UUID stale : active) { - debug.removeSubscriber(stale); + overlay.removeSubscriber(stale); } return viewers; } - private static int renderEntityBodies(@Nonnull Collection viewers, + private int renderEntityBodies(@Nonnull Collection viewers, @Nonnull Store store, - @Nonnull PhysicsWorldRuntimeResource resource, + @Nonnull Store physicsStore, @Nonnull Vector3d viewerPosition, double viewRadius, boolean debugShapes, @@ -218,25 +243,32 @@ private static int renderEntityBodies(@Nonnull Collection viewers, return 0; } double maxDistanceSquared = viewRadius * viewRadius; - for (PhysicsBodyRegistrationView registration : resource.getBodyRegistrationViews(PhysicsBodyKind.BODY)) { - if (!resource.hasBodyAttachments(registration.bodyKey())) { + for (UUID bodyUuid : PhysicsBodies.bodyUuids(physicsStore)) { + if (PhysicsChunkCollision.isChunkCollisionBody(physicsStore, bodyUuid)) { + continue; + } + Collection> attachments = PhysicsEntityAttachments.attachments(store, + bodyUuid, + null); + if (attachments.isEmpty()) { continue; } - for (Ref attachmentRef : resource.getBodyAttachments(registration.bodyKey())) { + for (Ref attachmentRef : attachments) { if (!attachmentRef.isValid()) { continue; } - PhysicsBodyAttachmentComponent attachment = store.getComponent(attachmentRef, - ATTACHMENT_TYPE); - TransformComponent transform = store.getComponent(attachmentRef, TRANSFORM_TYPE); + BodyAttachmentComponent attachment = store.getComponent(attachmentRef, + attachmentType); + TransformComponent transform = store.getComponent(attachmentRef, transformType); if (attachment == null || attachment.getLifecycle() == AttachmentLifecycle.GENERATED_PROXY || transform == null) { continue; } - PhysicsBodySnapshot snapshot = resource.getBodySnapshotIfRegistered(registration.bodyKey()); + PhysicsBodySnapshot snapshot = apiSnapshot(physicsStore, + PhysicsBodies.snapshot(physicsStore, bodyUuid)); if (snapshot == null) { continue; } @@ -267,7 +299,8 @@ private static int renderEntityBodies(@Nonnull Collection viewers, } private static int renderDetachedBodies(@Nonnull Collection viewers, - @Nonnull PhysicsWorldRuntimeResource resource, + @Nonnull Store store, + @Nonnull Store physicsStore, @Nonnull Vector3d viewerPosition, double viewRadius, boolean debugShapes, @@ -280,14 +313,14 @@ private static int renderDetachedBodies(@Nonnull Collection viewers, RenderedBodyCount rendered = new RenderedBodyCount(); double maxDistanceSquared = viewRadius * viewRadius; - for (PhysicsSpaceBinding space : resource.getSpaceBindings()) { - resource.forEachIndexedBodySnapshot(space.spaceId(), (bodyKey, snapshot, spaceId, kind, persistenceMode) -> { + for (SpaceId spaceId : PhysicsSpaces.spaceIds(physicsStore)) { + forEachBodySnapshot(physicsStore, spaceId, (bodyUuid, snapshot) -> { if (rendered.hasReached(maxBodies)) { return; } - if (kind != PhysicsBodyKind.BODY - || resource.hasBodyAttachments(bodyKey)) { + if (PhysicsChunkCollision.isChunkCollisionBody(physicsStore, bodyUuid) + || PhysicsEntityAttachments.hasAttachments(store, bodyUuid, null)) { return; } @@ -335,10 +368,10 @@ private int value() { } private static void renderSpaceOnlyShapes(@Nonnull Collection viewers, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull PhysicsSpaceBinding space, + @Nonnull Store physicsStore, + @Nonnull SpaceId spaceId, float time) { - resource.forEachIndexedBodySnapshot(space.spaceId(), (bodyKey, snapshot, spaceId, kind, persistenceMode) -> { + forEachBodySnapshot(physicsStore, spaceId, (_, snapshot) -> { if (snapshot.shapeType() != ShapeType.PLANE) { return; } @@ -355,24 +388,131 @@ private static void renderSpaceOnlyShapes(@Nonnull Collection viewers }); } + private static void forEachBodySnapshot(@Nonnull Store physicsStore, + @Nonnull SpaceId spaceId, + @Nonnull BodySnapshotConsumer consumer) { + UUID spaceUuid = physicsStore + .getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .getSpaceUuid(spaceId); + if (spaceUuid == null) { + return; + } + for (dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot snapshot + : PhysicsBodies.snapshotFrame(physicsStore).bodies()) { + if (!spaceUuid.equals(snapshot.spaceUuid())) { + continue; + } + PhysicsBodySnapshot apiSnapshot = apiSnapshot(physicsStore, snapshot); + if (apiSnapshot != null) { + consumer.accept(snapshot.bodyUuid(), apiSnapshot); + } + } + } + + @Nullable + private static PhysicsBodySnapshot apiSnapshot(@Nonnull Store store, + @Nullable dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot snapshot) { + if (snapshot == null) { + return null; + } + Ref ref = snapshot.bodyRef(); + if (ref == null || ref.getStore() != store || !ref.isValid()) { + ref = PhysicsEntities.resolveRef(store, snapshot.bodyUuid()); + } + boolean validRef = ref != null && ref.isValid(); + DynamicsComponent dynamics = validRef + ? store.getComponent(ref, DynamicsComponent.getComponentType()) + : null; + ColliderComponent collider = validRef + ? store.getComponent(ref, ColliderComponent.getComponentType()) + : null; + MaterialComponent material = validRef + ? store.getComponent(ref, MaterialComponent.getComponentType()) + : null; + CollisionFilterComponent filter = validRef + ? store.getComponent(ref, CollisionFilterComponent.getComponentType()) + : null; + ShapeComponent shape = validRef + ? store.getComponent(ref, ShapeComponent.getComponentType()) + : null; + + Vector3f position = snapshot.position(); + Quaterniond rotationD = new Quaterniond(snapshot.rotationX(), + snapshot.rotationY(), + snapshot.rotationZ(), + snapshot.rotationW()); + org.joml.Quaternionf rotation = new org.joml.Quaternionf((float) rotationD.x, + (float) rotationD.y, + (float) rotationD.z, + (float) rotationD.w); + Vector3f linearVelocity = snapshot.linearVelocity(); + Vector3f angularVelocity = snapshot.angularVelocity(); + PhysicsBodyType bodyType = snapshot.bodyType(); + ShapeType shapeType = shape != null ? shape.getShapeType() : ShapeType.UNKNOWN; + boolean hasBoxHalfExtents = shapeType == ShapeType.BOX && shape != null; + + return PhysicsBodySnapshot.of(position.x, + position.y, + position.z, + rotation.x, + rotation.y, + rotation.z, + rotation.w, + linearVelocity.x, + linearVelocity.y, + linearVelocity.z, + angularVelocity.x, + angularVelocity.y, + angularVelocity.z, + bodyType, + snapshot.sleeping(), + collider != null && collider.isSensor(), + bodyType == PhysicsBodyType.DYNAMIC ? authoredMass(dynamics) : 0.0f, + material != null ? material.getFriction() : 0.5f, + material != null ? material.getRestitution() : 0.0f, + dynamics != null ? dynamics.getLinearDamping() : 0.0f, + dynamics != null ? dynamics.getAngularDamping() : 0.0f, + filter != null ? filter.getCollisionGroup() : PhysicsCollisionFilters.DYNAMIC_BODY, + filter != null ? filter.getCollisionMask() : PhysicsCollisionFilters.ALL, + dynamics != null && dynamics.isContinuousCollisionEnabled(), + snapshot.centerOfMassOffsetY(), + shapeType, + hasBoxHalfExtents, + hasBoxHalfExtents ? shape.getHalfExtentX() : 0.0f, + hasBoxHalfExtents ? shape.getHalfExtentY() : 0.0f, + hasBoxHalfExtents ? shape.getHalfExtentZ() : 0.0f, + shape != null ? shape.getRadius() : 0.0f, + shape != null ? shape.getHalfHeight() : 0.0f, + shape != null ? shape.getAxis() : PhysicsAxis.Y); + } + + private static float authoredMass(@Nullable DynamicsComponent dynamics) { + return dynamics != null ? dynamics.getMass() : 1.0f; + } + + @FunctionalInterface + private interface BodySnapshotConsumer { + + void accept(@Nonnull UUID bodyUuid, @Nonnull PhysicsBodySnapshot snapshot); + } + private static void renderContacts(@Nonnull Collection viewers, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull PhysicsSpaceBinding space, + @Nonnull Store physicsStore, + @Nonnull SpaceId spaceId, @Nonnull UUID viewerUuid, @Nonnull DebugQueryCache queryCache, @Nonnull Vector3d viewerPosition, double viewRadius, int maxContacts, float time) { - DebugQueryKey key = DebugQueryKey.contacts(space.spaceId(), viewerUuid); + DebugQueryKey key = DebugQueryKey.contacts(spaceId, viewerUuid); try { queryCache.requestContactsIfIdle(key, - () -> resource.queryInternal(new PhysicsDebugContactsQuery(space.spaceId(), - viewerPosition.x, - viewerPosition.y, - viewerPosition.z, + () -> PhysicsStoreDebugQueries.contactsAsync(physicsStore, + spaceId, + viewerPosition, viewRadius, - maxContacts))); + maxContacts)); } catch (RuntimeException exception) { return; } @@ -382,23 +522,22 @@ private static void renderContacts(@Nonnull Collection viewers, } private static void renderJoints(@Nonnull Collection viewers, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull PhysicsSpaceBinding space, + @Nonnull Store physicsStore, + @Nonnull SpaceId spaceId, @Nonnull UUID viewerUuid, @Nonnull DebugQueryCache queryCache, @Nonnull Vector3d viewerPosition, double viewRadius, int maxJoints, float time) { - DebugQueryKey key = DebugQueryKey.joints(space.spaceId(), viewerUuid); + DebugQueryKey key = DebugQueryKey.joints(spaceId, viewerUuid); try { queryCache.requestJointsIfIdle(key, - () -> resource.queryInternal(new PhysicsDebugJointsQuery(space.spaceId(), - viewerPosition.x, - viewerPosition.y, - viewerPosition.z, + () -> PhysicsStoreDebugQueries.jointsAsync(physicsStore, + spaceId, + viewerPosition, viewRadius, - maxJoints))); + maxJoints)); } catch (RuntimeException exception) { return; } @@ -407,23 +546,38 @@ private static void renderJoints(@Nonnull Collection viewers, } } - private static void renderWorldCollision(@Nonnull Collection viewers, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull PhysicsSpaceBinding space, + private static void renderPhysicsChunkCollision(@Nonnull Collection viewers, + @Nonnull Store physicsStore, + @Nonnull SpaceId spaceId, + @Nonnull UUID viewerUuid, + @Nonnull DebugQueryCache queryCache, @Nonnull Vector3d viewerPosition, double viewRadius, int maxSections, int maxBoxes, float time) { + DebugQueryKey key = DebugQueryKey.physicsChunk(spaceId, viewerUuid); + try { + queryCache.requestPhysicsChunkIfIdle(key, + () -> PhysicsStoreDebugQueries.physicsChunkSectionsAsync(physicsStore, + spaceId, + viewerPosition, + viewRadius)); + } catch (RuntimeException exception) { + return; + } + double maxDistanceSquared = viewRadius * viewRadius; - List visibleSections = collectVisibleWorldCollisionSections( - resource, space, viewerPosition, maxDistanceSquared); + List visibleSections = collectVisiblePhysicsChunkSections( + queryCache.physicsChunkSectionsOrEmpty(key), + viewerPosition, + maxDistanceSquared); visibleSections.sort(Comparator.comparingDouble(VisibleDebugSection::distanceSquared)); int sectionLimit = Math.min(maxSections, visibleSections.size()); for (int i = 0; i < sectionLimit; i++) { - DebugSection section = visibleSections.get(i).section(); - PhysicsDebugRenderer.renderWorldCollisionSection(viewers, + PhysicsChunkDebugSectionView section = visibleSections.get(i).section(); + PhysicsDebugRenderer.renderPhysicsChunkCollisionSection(viewers, section.chunkX(), section.sectionY(), section.chunkZ(), @@ -431,14 +585,14 @@ private static void renderWorldCollision(@Nonnull Collection viewers, time); } - List visibleBoxes = collectVisibleWorldCollisionBoxes( + List visibleBoxes = collectVisiblePhysicsChunkBoxes( visibleSections, viewerPosition, maxDistanceSquared); visibleBoxes.sort(Comparator.comparingDouble(VisibleDebugBox::distanceSquared)); int boxLimit = Math.min(maxBoxes, visibleBoxes.size()); for (int i = 0; i < boxLimit; i++) { VisibleDebugBox visibleBox = visibleBoxes.get(i); - PhysicsDebugRenderer.renderWorldCollisionBox(viewers, + PhysicsDebugRenderer.renderPhysicsChunkCollisionBox(viewers, visibleBox.box(), visibleBox.color(), time); @@ -446,37 +600,36 @@ private static void renderWorldCollision(@Nonnull Collection viewers, } @Nonnull - private static List collectVisibleWorldCollisionSections( - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull PhysicsSpaceBinding space, + private static List collectVisiblePhysicsChunkSections( + @Nonnull Iterable sections, @Nonnull Vector3d viewerPosition, double maxDistanceSquared) { List visibleSections = new ArrayList<>(); - resource.worldCollisionCache().forEachDebugSection(space.spaceId(), section -> { + for (PhysicsChunkDebugSectionView section : sections) { double distanceSquared = distanceSquaredToSection(viewerPosition, section); if (distanceSquared > maxDistanceSquared) { - return; + continue; } visibleSections.add(new VisibleDebugSection(section, distanceSquared)); - }); + } return visibleSections; } @Nonnull - private static List collectVisibleWorldCollisionBoxes( + private static List collectVisiblePhysicsChunkBoxes( @Nonnull List visibleSections, @Nonnull Vector3d viewerPosition, double maxDistanceSquared) { List visibleBoxes = new ArrayList<>(); for (VisibleDebugSection visibleSection : visibleSections) { - DebugSection section = visibleSection.section(); - collectVisibleWorldCollisionBoxes(viewerPosition, + PhysicsChunkDebugSectionView section = visibleSection.section(); + collectVisiblePhysicsChunkBoxes(viewerPosition, maxDistanceSquared, section.fullCubeBoxes(), DebugUtils.COLOR_CYAN, visibleBoxes); - collectVisibleWorldCollisionBoxes(viewerPosition, + collectVisiblePhysicsChunkBoxes(viewerPosition, maxDistanceSquared, section.detailBoxes(), DebugUtils.COLOR_MAGENTA, @@ -485,7 +638,7 @@ private static List collectVisibleWorldCollisionBoxes( return visibleBoxes; } - private static void collectVisibleWorldCollisionBoxes(@Nonnull Vector3d viewerPosition, + private static void collectVisiblePhysicsChunkBoxes(@Nonnull Vector3d viewerPosition, double maxDistanceSquared, @Nonnull Iterable boxes, @Nonnull Vector3f color, @@ -501,7 +654,7 @@ private static void collectVisibleWorldCollisionBoxes(@Nonnull Vector3d viewerPo } private static double distanceSquaredToSection(@Nonnull Vector3d viewerPosition, - @Nonnull DebugSection section) { + @Nonnull PhysicsChunkDebugSectionView section) { double minX = section.chunkX() << ChunkUtil.BITS; double minY = section.sectionY() << ChunkUtil.BITS; double minZ = section.chunkZ() << ChunkUtil.BITS; @@ -552,7 +705,7 @@ static final class DebugQueryCache { /* * Debug overlays run in the tick path, so contact/joint queries are cached and polled. - * If an owner query is still incomplete, the renderer uses the previous completed result + * If a store tick query is still incomplete, the renderer uses the previous completed result * or skips that overlay for the frame instead of joining the world thread. */ @Nonnull @@ -567,6 +720,12 @@ static final class DebugQueryCache { @Nonnull private final Map> completedJoints = new Object2ObjectOpenHashMap<>(); + @Nonnull + private final Map>> + pendingPhysicsChunkSections = new Object2ObjectOpenHashMap<>(); + @Nonnull + private final Map> + completedPhysicsChunkSections = new Object2ObjectOpenHashMap<>(); synchronized boolean requestContactsIfIdle(@Nonnull DebugQueryKey key, @Nonnull Supplier>> completionSupplier) { @@ -600,6 +759,24 @@ synchronized List jointsOrEmpty(@Nonnull DebugQueryKey ke return completedJoints.getOrDefault(key, List.of()); } + synchronized boolean requestPhysicsChunkIfIdle(@Nonnull DebugQueryKey key, + @Nonnull Supplier>> + completionSupplier) { + pollPhysicsChunk(key); + if (pendingPhysicsChunkSections.containsKey(key)) { + return false; + } + pendingPhysicsChunkSections.put(key, completionSupplier.get().toCompletableFuture()); + return true; + } + + @Nonnull + synchronized List physicsChunkSectionsOrEmpty( + @Nonnull DebugQueryKey key) { + pollPhysicsChunk(key); + return completedPhysicsChunkSections.getOrDefault(key, List.of()); + } + private void pollContacts(@Nonnull DebugQueryKey key) { CompletableFuture> pending = pendingContacts.get(key); if (pending == null || !pending.isDone()) { @@ -618,6 +795,16 @@ private void pollJoints(@Nonnull DebugQueryKey key) { completedJoints.put(key, completedList(pending)); } + private void pollPhysicsChunk(@Nonnull DebugQueryKey key) { + CompletableFuture> pending = + pendingPhysicsChunkSections.get(key); + if (pending == null || !pending.isDone()) { + return; + } + pendingPhysicsChunkSections.remove(key); + completedPhysicsChunkSections.put(key, completedList(pending)); + } + @Nonnull private static List completedList(@Nonnull CompletableFuture> future) { try { @@ -647,14 +834,21 @@ static DebugQueryKey contacts(@Nonnull SpaceId spaceId, @Nonnull UUID viewerUuid static DebugQueryKey joints(@Nonnull SpaceId spaceId, @Nonnull UUID viewerUuid) { return new DebugQueryKey(QueryKind.JOINTS, spaceId, viewerUuid); } + + @Nonnull + static DebugQueryKey physicsChunk(@Nonnull SpaceId spaceId, @Nonnull UUID viewerUuid) { + return new DebugQueryKey(QueryKind.PHYSICS_CHUNK, spaceId, viewerUuid); + } } enum QueryKind { CONTACTS, - JOINTS + JOINTS, + PHYSICS_CHUNK } - private record VisibleDebugSection(@Nonnull DebugSection section, double distanceSquared) { + private record VisibleDebugSection(@Nonnull PhysicsChunkDebugSectionView section, + double distanceSquared) { } private record VisibleDebugBox(@Nonnull BoxCollider box, diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsStoreDebugQueries.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsStoreDebugQueries.java new file mode 100644 index 00000000..cbf2e0ea --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsStoreDebugQueries.java @@ -0,0 +1,595 @@ +package dev.hytalemodding.impulse.core.internal.systems.debug; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.util.ChunkUtil; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.SectionCollisionGeometry.BoxCollider; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionPayloadResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionPayload; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent.PartKind; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletionStage; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Quaternionf; +import org.joml.Vector3d; +import org.joml.Vector3f; + +/** + * Queued PhysicsStore reads for debug overlay contact and joint views. + */ +final class PhysicsStoreDebugQueries { + + private static final float CONTACT_NORMAL_SCALE = 0.75f; + private static final float JOINT_AXIS_SCALE = 0.9f; + + private PhysicsStoreDebugQueries() { + } + + @Nonnull + static CompletionStage> contactsAsync( + @Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull Vector3d viewerPosition, + double viewRadius, + int maxContacts) { + double viewerX = viewerPosition.x; + double viewerY = viewerPosition.y; + double viewerZ = viewerPosition.z; + return PhysicsThreading.enqueueReadOnWorldThread(store, + "queue PhysicsStore debug contact read", + physics -> contacts(physics, + spaceId, + viewerX, + viewerY, + viewerZ, + viewRadius, + maxContacts)); + } + + @Nonnull + static CompletionStage> jointsAsync( + @Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull Vector3d viewerPosition, + double viewRadius, + int maxJoints) { + double viewerX = viewerPosition.x; + double viewerY = viewerPosition.y; + double viewerZ = viewerPosition.z; + return PhysicsThreading.enqueueReadOnWorldThread(store, + "queue PhysicsStore debug joint read", + physics -> joints(physics, + spaceId, + viewerX, + viewerY, + viewerZ, + viewRadius, + maxJoints)); + } + + @Nonnull + static CompletionStage> physicsChunkSectionsAsync( + @Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull Vector3d viewerPosition, + double viewRadius) { + double viewerX = viewerPosition.x; + double viewerY = viewerPosition.y; + double viewerZ = viewerPosition.z; + return PhysicsThreading.enqueueReadOnWorldThread(store, + "queue PhysicsStore PhysicsChunk collision debug read", + physics -> physicsChunkSections(physics, + spaceId, + viewerX, + viewerY, + viewerZ, + viewRadius)); + } + + @Nonnull + static List contacts(@Nonnull Store store, + @Nonnull SpaceId spaceId, + double viewerX, + double viewerY, + double viewerZ, + double viewRadius, + int maxContacts) { + PhysicsThreading.requireWorldThread(store, "read PhysicsStore debug contacts"); + int limit = Math.max(0, maxContacts); + if (limit == 0) { + return List.of(); + } + + SpaceContext space = space(store, spaceId); + if (space == null) { + return List.of(); + } + + double maxDistanceSquared = viewRadius * viewRadius; + List visible = new ArrayList<>(Math.min(limit, 64)); + space.backendRuntime().contacts(space.spaceHandle().value(), limit, (bodyAId, + bodyBId, + pointAX, + pointAY, + pointAZ, + pointBX, + pointBY, + pointBZ, + normalBX, + normalBY, + normalBZ, + distance, + impulse) -> { + if (visible.size() >= limit) { + return; + } + if (distanceSquared(pointBX, pointBY, pointBZ, viewerX, viewerY, viewerZ) + > maxDistanceSquared) { + return; + } + visible.add(toDebugContactView(pointBX, + pointBY, + pointBZ, + normalBX, + normalBY, + normalBZ, + impulse)); + }); + return List.copyOf(visible); + } + + @Nonnull + private static List joints(@Nonnull Store store, + @Nonnull SpaceId spaceId, + double viewerX, + double viewerY, + double viewerZ, + double viewRadius, + int maxJoints) { + PhysicsThreading.requireWorldThread(store, "read PhysicsStore debug joints"); + int limit = Math.max(0, maxJoints); + if (limit == 0) { + return List.of(); + } + + UUID spaceUuid = store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .getSpaceUuid(spaceId); + if (spaceUuid == null) { + return List.of(); + } + Ref spaceRef = store.getExternalData().getRefFromUUID(spaceUuid); + + PhysicsSnapshotResource snapshots = store.getResource(PhysicsSnapshotResource.getResourceType()); + double maxDistanceSquared = viewRadius * viewRadius; + List visible = new ArrayList<>(Math.min(limit, 64)); + BiConsumer, CommandBuffer> collector = + (chunk, _) -> collectJointChunk(chunk, + snapshots, + spaceRef, + spaceUuid, + viewerX, + viewerY, + viewerZ, + maxDistanceSquared, + limit, + visible); + store.forEachChunk(JointComponent.getComponentType(), collector); + return List.copyOf(visible); + } + + @Nonnull + private static List physicsChunkSections( + @Nonnull Store store, + @Nonnull SpaceId spaceId, + double viewerX, + double viewerY, + double viewerZ, + double viewRadius) { + PhysicsThreading.requireWorldThread(store, + "read PhysicsStore PhysicsChunk collision debug sections"); + SpaceContext spaceContext = space(store, spaceId); + if (spaceContext == null) { + return List.of(); + } + + UUID spaceUuid = store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .getSpaceUuid(spaceId); + if (spaceUuid == null) { + return List.of(); + } + Ref spaceRef = store.getExternalData().getRefFromUUID(spaceUuid); + + PhysicsChunkCollisionPayloadResource payloads = store.getResource( + PhysicsChunkCollisionPayloadResource.getResourceType()); + double maxDistanceSquared = viewRadius * viewRadius; + Map sections = + new Object2ObjectOpenHashMap<>(); + BiConsumer, CommandBuffer> collector = + (chunk, _) -> collectPhysicsChunkSourceChunk(chunk, + payloads, + spaceContext, + spaceRef, + spaceUuid, + viewerX, + viewerY, + viewerZ, + maxDistanceSquared, + sections); + store.forEachChunk(ChunkCollisionSourceComponent.getComponentType(), collector); + List visible = new ArrayList<>(sections.size()); + sections.values() + .forEach(builder -> visible.add(builder.toView())); + return List.copyOf(visible); + } + + private static void collectPhysicsChunkSourceChunk(@Nonnull ArchetypeChunk chunk, + @Nonnull PhysicsChunkCollisionPayloadResource payloads, + @Nonnull SpaceContext spaceContext, + @Nullable Ref spaceRef, + @Nonnull UUID spaceUuid, + double viewerX, + double viewerY, + double viewerZ, + double maxDistanceSquared, + @Nonnull Map sections) { + for (int index = 0; index < chunk.size(); index++) { + ChunkCollisionSourceComponent source = chunk.getComponent(index, + ChunkCollisionSourceComponent.getComponentType()); + BodyComponent body = chunk.getComponent(index, BodyComponent.getComponentType()); + if (source == null || body == null || !matchesSpace(body, spaceRef, spaceUuid)) { + continue; + } + if (distanceSquaredToSection(source, viewerX, viewerY, viewerZ) + > maxDistanceSquared) { + continue; + } + if (source.getPartKind() == PartKind.NATIVE_VOXELS) { + collectNativeVoxels(payloads, spaceContext, source, sections); + } else { + collectBoxCollision(chunk, index, source, sections); + } + } + } + + private static void collectNativeVoxels( + @Nonnull PhysicsChunkCollisionPayloadResource payloads, + @Nonnull SpaceContext spaceContext, + @Nonnull ChunkCollisionSourceComponent source, + @Nonnull Map sections) { + ChunkCollisionPayload payload = payloads.get(source.getPayloadResourceKey()); + if (payload == null || payload.isEmpty()) { + return; + } + boolean nativeVoxels = payload.nativeVoxelCollisionEnabled() + && payload.hasFullCubeVoxels() + && spaceContext.backendRuntime() + .supportsVoxelTerrain(spaceContext.spaceHandle().value()); + if (nativeVoxels) { + section(sections, source).voxelTerrain = true; + } + } + + private static void collectBoxCollision(@Nonnull ArchetypeChunk chunk, + int index, + @Nonnull ChunkCollisionSourceComponent source, + @Nonnull Map sections) { + BoxCollider box = rowBox(chunk, index); + if (box == null) { + return; + } + PhysicsChunkDebugSectionBuilder section = section(sections, source); + if (source.getPartKind() == PartKind.BOX) { + section.fullCubeBoxes.add(box); + } else if (source.getPartKind() == PartKind.DETAIL_BOX) { + section.detailBoxes.add(box); + } + } + + @Nullable + private static BoxCollider rowBox(@Nonnull ArchetypeChunk chunk, int index) { + ShapeComponent shape = chunk.getComponent(index, ShapeComponent.getComponentType()); + TargetComponent target = chunk.getComponent(index, TargetComponent.getComponentType()); + if (shape == null || target == null || shape.getShapeType() != ShapeType.BOX) { + return null; + } + Vector3f position = target.getPosition(); + return new BoxCollider(position.x, + position.y, + position.z, + shape.getHalfExtentX(), + shape.getHalfExtentY(), + shape.getHalfExtentZ()); + } + + @Nonnull + private static PhysicsChunkDebugSectionBuilder section( + @Nonnull Map sections, + @Nonnull ChunkCollisionSourceComponent source) { + SectionKey key = new SectionKey(source.getChunkX(), + source.getSectionY(), + source.getChunkZ()); + return sections.computeIfAbsent(key, _ -> new PhysicsChunkDebugSectionBuilder(key)); + } + + private static void collectJointChunk(@Nonnull ArchetypeChunk chunk, + @Nonnull PhysicsSnapshotResource snapshots, + @Nullable Ref spaceRef, + @Nonnull UUID spaceUuid, + double viewerX, + double viewerY, + double viewerZ, + double maxDistanceSquared, + int limit, + @Nonnull List visible) { + for (int index = 0; index < chunk.size(); index++) { + if (visible.size() >= limit) { + return; + } + JointComponent joint = chunk.getComponent(index, JointComponent.getComponentType()); + if (joint == null || !matchesSpace(joint, spaceRef, spaceUuid) || !joint.isEnabled()) { + continue; + } + PhysicsDebugJointView view = toDebugJointView(joint, snapshots); + if (view == null) { + continue; + } + double midpointX = (view.anchorAX() + view.anchorBX()) * 0.5; + double midpointY = (view.anchorAY() + view.anchorBY()) * 0.5; + double midpointZ = (view.anchorAZ() + view.anchorBZ()) * 0.5; + if (distanceSquared(midpointX, midpointY, midpointZ, viewerX, viewerY, viewerZ) + > maxDistanceSquared) { + continue; + } + visible.add(view); + } + } + + @Nullable + private static PhysicsDebugJointView toDebugJointView(@Nonnull JointComponent joint, + @Nonnull PhysicsSnapshotResource snapshots) { + PhysicsBodySnapshot bodyA = + bodySnapshot(snapshots, joint.getBodyARef(), joint.getBodyAUuid()); + PhysicsBodySnapshot bodyB = + bodySnapshot(snapshots, joint.getBodyBRef(), joint.getBodyBUuid()); + if (bodyA == null || bodyB == null) { + return null; + } + + Vector3f anchorA = worldAnchor(bodyA, joint.getAnchorA()); + Vector3f anchorB = worldAnchor(bodyB, joint.getAnchorB()); + Vector3f axis = joint.getAxis(); + if (axis.lengthSquared() <= 0.0f) { + return new PhysicsDebugJointView(anchorA.x, + anchorA.y, + anchorA.z, + anchorB.x, + anchorB.y, + anchorB.z, + false, + 0.0f, + 0.0f, + 0.0f); + } + + Vector3f worldAxis = new Vector3f(axis).normalize().mul(JOINT_AXIS_SCALE); + bodyA.rotation().transform(worldAxis); + return new PhysicsDebugJointView(anchorA.x, + anchorA.y, + anchorA.z, + anchorB.x, + anchorB.y, + anchorB.z, + true, + worldAxis.x, + worldAxis.y, + worldAxis.z); + } + + private static boolean matchesSpace(@Nonnull JointComponent joint, + @Nullable Ref spaceRef, + @Nonnull UUID spaceUuid) { + Ref jointSpaceRef = joint.getSpaceRef(); + if (jointSpaceRef != null && spaceRef != null) { + return sameRef(jointSpaceRef, spaceRef); + } + return spaceUuid.equals(joint.getSpaceUuid()); + } + + private static boolean matchesSpace(@Nonnull BodyComponent body, + @Nullable Ref spaceRef, + @Nonnull UUID spaceUuid) { + Ref bodySpaceRef = body.getSpaceRef(); + if (bodySpaceRef != null && spaceRef != null) { + return sameRef(bodySpaceRef, spaceRef); + } + return spaceUuid.equals(body.getSpaceUuid()); + } + + @Nullable + private static PhysicsBodySnapshot bodySnapshot( + @Nonnull PhysicsSnapshotResource snapshots, + @Nullable Ref bodyRef, + @Nonnull UUID bodyUuid) { + return bodyRef != null ? snapshots.getBody(bodyRef) : snapshots.getBody(bodyUuid); + } + + private static boolean sameRef(@Nonnull Ref first, + @Nonnull Ref second) { + return first.getStore() == second.getStore() + && first.getIndex() == second.getIndex(); + } + + @Nonnull + private static Vector3f worldAnchor(@Nonnull PhysicsBodySnapshot body, + @Nonnull Vector3f localAnchor) { + Vector3f anchor = new Vector3f(localAnchor); + Quaternionf rotation = body.rotation(); + rotation.transform(anchor); + return anchor.add(body.position()); + } + + @Nonnull + private static PhysicsDebugContactView toDebugContactView(float pointX, + float pointY, + float pointZ, + float normalX, + float normalY, + float normalZ, + float impulse) { + Vector3f normal = new Vector3f(normalX, normalY, normalZ); + if (normal.lengthSquared() <= 0.0f) { + return new PhysicsDebugContactView(pointX, + pointY, + pointZ, + false, + 0.0f, + 0.0f, + 0.0f); + } + + float magnitude = Math.max(CONTACT_NORMAL_SCALE, Math.abs(impulse) * 0.05f); + normal.normalize().mul(magnitude); + return new PhysicsDebugContactView(pointX, + pointY, + pointZ, + true, + normal.x, + normal.y, + normal.z); + } + + @Nullable + private static SpaceContext space(@Nonnull Store store, + @Nonnull SpaceId spaceId) { + PhysicsSpaceCompatibilityIndexResource compatibility = store.getResource( + PhysicsSpaceCompatibilityIndexResource.getResourceType()); + UUID spaceUuid = compatibility.getSpaceUuid(spaceId); + if (spaceUuid == null) { + return null; + } + Ref spaceRef = store.getExternalData().getRefFromUUID(spaceUuid); + if (spaceRef == null || spaceRef.getStore() != store || !spaceRef.isValid()) { + return null; + } + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + BackendSpaceHandle spaceHandle = runtime.getSpaceHandle(spaceRef); + BackendId backendId = runtime.getSpaceBackendId(spaceRef); + PhysicsBackendRuntime backendRuntime = + backendId != null ? runtime.getRuntime(backendId) : null; + if (spaceHandle == null || backendRuntime == null) { + return null; + } + return new SpaceContext(spaceHandle, backendRuntime); + } + + private static double distanceSquared(double x, + double y, + double z, + double viewerX, + double viewerY, + double viewerZ) { + double dx = x - viewerX; + double dy = y - viewerY; + double dz = z - viewerZ; + return dx * dx + dy * dy + dz * dz; + } + + private static double distanceSquaredToSection(@Nonnull ChunkCollisionSourceComponent source, + double viewerX, + double viewerY, + double viewerZ) { + double minX = source.getChunkX() << ChunkUtil.BITS; + double minY = source.getSectionY() << ChunkUtil.BITS; + double minZ = source.getChunkZ() << ChunkUtil.BITS; + return distanceSquaredToBounds(viewerX, + viewerY, + viewerZ, + minX, + minY, + minZ, + minX + ChunkUtil.SIZE, + minY + ChunkUtil.SIZE, + minZ + ChunkUtil.SIZE); + } + + private static double distanceSquaredToBounds(double viewerX, + double viewerY, + double viewerZ, + double minX, + double minY, + double minZ, + double maxX, + double maxY, + double maxZ) { + double dx = axisDistance(viewerX, minX, maxX); + double dy = axisDistance(viewerY, minY, maxY); + double dz = axisDistance(viewerZ, minZ, maxZ); + return dx * dx + dy * dy + dz * dz; + } + + private static double axisDistance(double value, double min, double max) { + if (value < min) { + return min - value; + } + if (value > max) { + return value - max; + } + return 0.0; + } + + private record SpaceContext(@Nonnull BackendSpaceHandle spaceHandle, + @Nonnull PhysicsBackendRuntime backendRuntime) { + } + + private record SectionKey(int chunkX, int sectionY, int chunkZ) { + } + + private static final class PhysicsChunkDebugSectionBuilder { + + @Nonnull + private final SectionKey key; + @Nonnull + private final List fullCubeBoxes = new ArrayList<>(); + @Nonnull + private final List detailBoxes = new ArrayList<>(); + private boolean voxelTerrain; + + private PhysicsChunkDebugSectionBuilder(@Nonnull SectionKey key) { + this.key = key; + } + + @Nonnull + private PhysicsChunkDebugSectionView toView() { + return new PhysicsChunkDebugSectionView(key.chunkX(), + key.sectionY(), + key.chunkZ(), + voxelTerrain, + fullCubeBoxes, + detailBoxes); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/owner/PhysicsOwnerLifecycleSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/owner/PhysicsOwnerLifecycleSystem.java deleted file mode 100644 index 200dfd7d..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/owner/PhysicsOwnerLifecycleSystem.java +++ /dev/null @@ -1,127 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.owner; - -import com.hypixel.hytale.component.ResourceType; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.system.StoreSystem; -import com.hypixel.hytale.logger.HytaleLogger; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerResource; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import java.util.ArrayList; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.logging.Level; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Starts and stops the per-world physics owner lane with the EntityStore lifecycle. - */ -public final class PhysicsOwnerLifecycleSystem extends StoreSystem - implements AutoCloseable { - - private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); - - @Nonnull - private final ResourceType ownerResourceType; - @Nonnull - private final ResourceType physicsWorldResourceType; - @Nonnull - private final Set activeOwners = ConcurrentHashMap.newKeySet(); - - public PhysicsOwnerLifecycleSystem() { - this(PhysicsOwnerResource.getResourceType(), PhysicsWorldResource.getResourceType()); - } - - PhysicsOwnerLifecycleSystem( - @Nonnull ResourceType ownerResourceType, - @Nonnull ResourceType physicsWorldResourceType) { - this.ownerResourceType = ownerResourceType; - this.physicsWorldResourceType = physicsWorldResourceType; - } - - @Override - public void onSystemAddedToStore(@Nonnull Store store) { - PhysicsOwnerResource owner = store.getResource(ownerResourceType); - if (startOwner(owner, worldName(store))) { - PhysicsWorldRuntimeResource.require(store.getResource(physicsWorldResourceType)) - .attachOwnerExecutor(owner); - } - } - - @Override - public void onSystemRemovedFromStore(@Nonnull Store store) { - String worldName = worldName(store); - PhysicsOwnerResource owner = store.getResource(ownerResourceType); - PhysicsWorldResource physics = store.getResource(physicsWorldResourceType); - PhysicsWorldRuntimeResource runtime = PhysicsWorldRuntimeResource.require(physics); - RuntimeException clearFailure = tryClearSpaces(physics, worldName); - boolean closedOwner = closeOwner(owner); - runtime.detachOwnerExecutor(owner); - - // Retry if it failed before, this could happen. - if (clearFailure != null && closedOwner) { - clearFailure = tryClearSpaces(physics, worldName); - } - if (clearFailure != null) { - LOGGER.at(Level.WARNING).log("Failed to clear physics spaces for world %s: %s", - worldName, - clearFailure.getMessage()); - } - } - - @Override - public void close() { - for (PhysicsOwnerResource owner : new ArrayList<>(activeOwners)) { - closeOwner(owner); - } - } - - boolean startOwner(@Nonnull PhysicsOwnerResource owner, @Nonnull String worldName) { - try { - owner.start(worldName); - if (owner.isStarted()) { - activeOwners.add(owner); - return true; - } - } catch (RuntimeException exception) { - LOGGER.at(Level.WARNING).log("Physics owner lane could not be started for world %s: %s", - worldName, - exception.getMessage()); - } - return false; - } - - boolean closeOwner(@Nonnull PhysicsOwnerResource owner) { - activeOwners.remove(owner); - try { - owner.close(); - return true; - } catch (RuntimeException exception) { - LOGGER.at(Level.WARNING).log("Failed to close physics owner lane: %s", - exception.getMessage()); - return false; - } - } - - int activeOwnerCount() { - return activeOwners.size(); - } - - @Nonnull - private static String worldName(@Nonnull Store store) { - return store.getExternalData().getWorld().getName(); - } - - @Nullable - private static RuntimeException tryClearSpaces(@Nonnull PhysicsWorldResource physics, - @Nonnull String worldName) { - try { - physics.clearAllSpaces(worldName); - return null; - } catch (RuntimeException exception) { - return exception; - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsBodyHydrationSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsBodyHydrationSystem.java deleted file mode 100644 index 125ec6a8..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsBodyHydrationSystem.java +++ /dev/null @@ -1,137 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.persistence; - -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.SystemGroup; -import com.hypixel.hytale.component.dependency.Dependency; -import com.hypixel.hytale.component.dependency.Order; -import com.hypixel.hytale.component.dependency.SystemDependency; -import com.hypixel.hytale.component.system.tick.TickingSystem; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsBodyState; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerBridge; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import java.util.Set; -import javax.annotation.Nonnull; - -/** - * Restores world-level body states keyed by {@link RigidBodyKey}. - */ -public class PersistentPhysicsBodyHydrationSystem extends TickingSystem { - - private static final Set> DEPENDENCIES = Set.of( - new SystemDependency<>(Order.AFTER, PersistentPhysicsSpaceBootstrapSystem.class) - ); - - @Nonnull - private final SystemGroup group = ImpulsePlugin.get().getPersistenceRestoreGroup(); - - @Override - public void tick(float dt, int systemIndex, @Nonnull Store store) { - PersistentPhysicsWorldResource persistent = store.getResource( - PersistentPhysicsWorldResource.getResourceType()); - if (!persistent.isRuntimeRestorePending() - || !persistent.isRuntimeSpaceBootstrapComplete() - || persistent.hasRuntimeRestoreFailed()) { - return; - } - - PhysicsWorldRuntimeResource runtime = PhysicsWorldRuntimeResource.require(store); - for (PersistentPhysicsBodyState state : persistent.getBodies()) { - RigidBodyKey bodyKey = state.getBodyKey(); - if (bodyKey == null) { - persistent.recordRuntimeBodySkipped("missing body key"); - continue; - } - - String validationFailure = state.restoreValidationFailureReason(); - if (validationFailure != null) { - persistent.recordRuntimeBodySkipped(bodyKey, validationFailure); - continue; - } - - int resolvedSpaceId = state.resolveSpaceId(); - if (resolvedSpaceId <= 0) { - persistent.recordRuntimeBodySkipped(bodyKey, "no resolved space id"); - continue; - } - - try { - RestoreBodyResult result = PhysicsOwnerBridge.call(store, - "hydrate persisted physics body", - () -> restoreBodyOnOwner(runtime, state, bodyKey)); - if (result == RestoreBodyResult.RESTORED) { - persistent.recordRuntimeBodyRestored(); - } else if (result == RestoreBodyResult.MISSING_SPACE) { - persistent.recordRuntimeBodySkipped(bodyKey, "missing target space"); - } - } catch (RuntimeException exception) { - persistent.failRuntimeRestore("Failed to hydrate persisted body " + bodyKey - + ": " + exception.getClass().getSimpleName()); - return; - } - } - } - - static RestoreBodyResult restoreBodyOnOwner(@Nonnull PhysicsWorldRuntimeResource runtime, - @Nonnull PersistentPhysicsBodyState state, - @Nonnull RigidBodyKey bodyKey) { - if (runtime.getRegistration(bodyKey) != null) { - return RestoreBodyResult.ALREADY_REGISTERED; - } - - PhysicsSpaceBinding space = runtime.getSpaceBinding(new SpaceId(state.resolveSpaceId())); - if (space == null) { - return RestoreBodyResult.MISSING_SPACE; - } - - BackendBodyHandle backendBodyHandle = state.createBackendBody(space); - try { - state.applyToBody(space, backendBodyHandle); - runtime.addBodyOnOwner(bodyKey, - space.spaceId(), - backendBodyHandle, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - } catch (RuntimeException exception) { - removeBackendBody(space, backendBodyHandle, exception); - throw exception; - } - return RestoreBodyResult.RESTORED; - } - - private static void removeBackendBody(@Nonnull PhysicsSpaceBinding space, - @Nonnull BackendBodyHandle backendBodyHandle, - @Nonnull RuntimeException restoreFailure) { - try { - space.runtime().removeBody(space.backendSpaceHandle().value(), backendBodyHandle.value()); - } catch (RuntimeException cleanupFailure) { - restoreFailure.addSuppressed(cleanupFailure); - } - } - - enum RestoreBodyResult { - RESTORED, - ALREADY_REGISTERED, - MISSING_SPACE - } - - @Nonnull - @Override - public Set> getDependencies() { - return DEPENDENCIES; - } - - @Nonnull - @Override - public SystemGroup getGroup() { - return group; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsJointHydrationSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsJointHydrationSystem.java deleted file mode 100644 index f538924c..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsJointHydrationSystem.java +++ /dev/null @@ -1,157 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.persistence; - -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.SystemGroup; -import com.hypixel.hytale.component.dependency.Dependency; -import com.hypixel.hytale.component.dependency.Order; -import com.hypixel.hytale.component.dependency.SystemDependency; -import com.hypixel.hytale.component.system.tick.TickingSystem; -import com.hypixel.hytale.logger.HytaleLogger; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsJointState; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsRuntimeSupport; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistration; -import dev.hytalemodding.impulse.core.internal.resources.joint.PhysicsJointRegistration; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerBridge; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -import java.util.Set; -import java.util.logging.Level; -import javax.annotation.Nonnull; - -/** - * Third stage of persistence restore: reconnects joints between hydrated bodies. - * - *

Runs when {@code runtimeRestorePending} is true. - * Scans the persisted joint definitions and creates each joint in the target space - * if both endpoint bodies have already been hydrated by body key.

- * - *

Uses deterministic key strings (from {@link PersistentPhysicsJointState#key()}) - * to avoid duplicating joints that already exist in the runtime space. Clears - * the {@code runtimeRestorePending} flag once all persisted joints have been - * restored or terminally skipped.

- * - *

Runs after space bootstrap and body hydration so that both spaces and bodies - * are available for joint creation.

- */ -public class PersistentPhysicsJointHydrationSystem extends TickingSystem { - - private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); - - private static final Set> DEPENDENCIES = Set.of( - new SystemDependency<>(Order.AFTER, PersistentPhysicsSpaceBootstrapSystem.class), - new SystemDependency<>(Order.AFTER, PersistentPhysicsBodyHydrationSystem.class) - ); - - @Nonnull - private final SystemGroup group = ImpulsePlugin.get().getPersistenceRestoreGroup(); - - @Override - public void tick(float dt, int systemIndex, @Nonnull Store store) { - PersistentPhysicsWorldResource persistent = store.getResource( - PersistentPhysicsWorldResource.getResourceType()); - if (!persistent.isRuntimeRestorePending()) { - return; - } - - PhysicsOwnerBridge.run(store, "hydrate persisted physics joints", () -> hydrateJoints(store, - persistent)); - - if (!shouldFinalizeRuntimeRestore(persistent)) { - if (persistent.hasRuntimeRestoreFailed()) { - LOGGER.at(Level.SEVERE).log(persistent.runtimeRestoreFailureSummary()); - } - return; - } - - World world = store.getExternalData().getWorld(); - PersistentPhysicsRestoreTerrainPrewarm.prewarmRestoredDynamicBodyTerrain(world, - PhysicsWorldRuntimeResource.require(store), - persistent, - Math.max(0L, world.getTick())); - persistent.clearRuntimeRestorePending(); - if (persistent.hasRuntimeRestoreSkips()) { - LOGGER.at(Level.WARNING).log(persistent.runtimeRestoreSummary()); - } else { - LOGGER.at(Level.INFO).log(persistent.runtimeRestoreSummary()); - } - } - - static boolean shouldFinalizeRuntimeRestore(@Nonnull PersistentPhysicsWorldResource persistent) { - return persistent.isRuntimeRestorePending() && !persistent.hasRuntimeRestoreFailed(); - } - - private static void hydrateJoints(@Nonnull Store store, - @Nonnull PersistentPhysicsWorldResource persistent) { - PhysicsWorldRuntimeResource runtime = PhysicsWorldRuntimeResource.require(store); - Set existing = new ObjectOpenHashSet<>(); - for (PhysicsJointRegistration joint : runtime.getJointRegistrations()) { - existing.add(PersistentPhysicsRuntimeSupport.jointKey(joint.spaceId().value(), - joint.bodyA(), - joint.bodyB(), - joint)); - } - - for (PersistentPhysicsJointState state : persistent.getJoints()) { - String key = state.key(); - RigidBodyKey bodyAKey = state.getBodyAKey(); - RigidBodyKey bodyBKey = state.getBodyBKey(); - if (bodyAKey == null || bodyBKey == null) { - persistent.recordRuntimeJointSkipped(key, "missing endpoint body key"); - continue; - } - - if (existing.contains(key)) { - continue; - } - - PhysicsSpaceBinding space = runtime.getSpaceBinding(new SpaceId(state.getSpaceId())); - PhysicsBodyRegistration bodyA = runtime.getRegistration(bodyAKey); - PhysicsBodyRegistration bodyB = runtime.getRegistration(bodyBKey); - if (space == null) { - persistent.recordRuntimeJointSkipped(key, "missing target space"); - continue; - } - if (bodyA == null || bodyB == null) { - if (bodyA == null && bodyB == null) { - persistent.recordRuntimeJointSkipped(key, "missing both endpoint bodies"); - } else if (bodyA == null) { - persistent.recordRuntimeJointSkipped(key, "missing body A"); - } else { - persistent.recordRuntimeJointSkipped(key, "missing body B"); - } - continue; - } - - try { - PersistentPhysicsRuntimeSupport.createJoint(runtime, space, state, bodyAKey, bodyA, bodyBKey, bodyB); - } catch (RuntimeException exception) { - persistent.failRuntimeRestore("Failed to hydrate persisted joint in space " - + state.getSpaceId() - + ": " - + exception.getClass().getSimpleName()); - return; - } - persistent.recordRuntimeJointRestored(); - existing.add(key); - } - } - - @Nonnull - @Override - public Set> getDependencies() { - return DEPENDENCIES; - } - - @Nonnull - @Override - public SystemGroup getGroup() { - return group; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsRestoreTerrainPrewarm.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsRestoreTerrainPrewarm.java deleted file mode 100644 index 3583d97f..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsRestoreTerrainPrewarm.java +++ /dev/null @@ -1,116 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.persistence; - -import com.hypixel.hytale.server.core.universe.world.World; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldCollisionLifecycle; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsBodyState; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldCollisionSettings; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import javax.annotation.Nonnull; -import org.joml.Vector3d; -import org.joml.Vector3f; - -final class PersistentPhysicsRestoreTerrainPrewarm { - - private PersistentPhysicsRestoreTerrainPrewarm() { - } - - static void prewarmRestoredDynamicBodyTerrain(@Nonnull World world, - @Nonnull PhysicsWorldRuntimeResource runtime, - @Nonnull PersistentPhysicsWorldResource persistent, - long tick) { - if (!WorldCollisionLifecycle.isEnabled()) { - return; - } - for (Map.Entry> entry : dynamicBodiesBySpace( - persistent.getBodies()).entrySet()) { - SpaceId spaceId = new SpaceId(entry.getKey()); - if (runtime.getSpaceBinding(spaceId) == null) { - continue; - } - PhysicsWorldCollisionSettings settings = - runtime.getLiveSpaceSettings(spaceId).getWorldCollisionSettings(); - if (settings.getWorldCollisionMode() != WorldCollisionMode.STREAMING) { - continue; - } - List targets = dynamicPrewarmTargets(entry.getValue(), - settings.getWorldCollisionBodyRadius()); - if (targets.isEmpty()) { - continue; - } - runtime.ensureWorldCollisionAround(world, - spaceId, - targets, - settings.getWorldCollisionBodyRadius(), - tick); - } - } - - @Nonnull - static Map> dynamicPrewarmTargetsBySpace( - @Nonnull PersistentPhysicsBodyState[] bodies, - int radius) { - Map> targetsBySpace = new LinkedHashMap<>(); - for (Map.Entry> entry : dynamicBodiesBySpace(bodies).entrySet()) { - targetsBySpace.put(entry.getKey(), dynamicPrewarmTargets(entry.getValue(), radius)); - } - return targetsBySpace; - } - - @Nonnull - private static Map> dynamicBodiesBySpace( - @Nonnull PersistentPhysicsBodyState[] bodies) { - Map> bodiesBySpace = new LinkedHashMap<>(); - for (PersistentPhysicsBodyState body : bodies) { - if (body.restoreValidationFailureReason() != null - || body.resolveSpaceId() <= 0 - || body.getBodyType() != PhysicsBodyType.DYNAMIC - || body.isSensor()) { - continue; - } - bodiesBySpace.computeIfAbsent(body.resolveSpaceId(), ignored -> new ArrayList<>()).add(body); - } - return bodiesBySpace; - } - - @Nonnull - private static List dynamicPrewarmTargets(@Nonnull List bodies, - int radius) { - List targets = new ArrayList<>(); - for (PersistentPhysicsBodyState body : bodies) { - addBodyTargets(targets, - body, - radius); - } - return targets; - } - - private static void addBodyTargets(@Nonnull List targets, - @Nonnull PersistentPhysicsBodyState body, - int radius) { - Vector3f position = body.getPosition(); - Vector3f velocity = body.getLinearVelocity(); - if (!Float.isFinite(velocity.y) || velocity.y >= 0.0f) { - targets.add(new Vector3d(position.x, position.y, position.z)); - return; - } - - double minCenterY = Math.min(position.y, Math.max(0, radius)); - double step = Math.max(1.0, Math.max(0, radius) * 2.0); - double lastY = Double.NaN; - for (double y = position.y; y >= minCenterY; y -= step) { - targets.add(new Vector3d(position.x, y, position.z)); - lastY = y; - } - if (Double.isNaN(lastY) || lastY > minCenterY) { - targets.add(new Vector3d(position.x, minCenterY, position.z)); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsSpaceBootstrapSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsSpaceBootstrapSystem.java deleted file mode 100644 index 3ed89068..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsSpaceBootstrapSystem.java +++ /dev/null @@ -1,173 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.persistence; - -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.RemoveReason; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.SystemGroup; -import com.hypixel.hytale.component.system.tick.TickingSystem; -import com.hypixel.hytale.logger.HytaleLogger; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.internal.components.GeneratedVisualProxyComponent; -import dev.hytalemodding.impulse.core.internal.modules.control.components.PhysicsControlSessionComponent; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsRestorePreflight; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsSpaceState; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerBridge; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent.AttachmentLifecycle; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import java.util.logging.Level; -import javax.annotation.Nonnull; - -/** - * First stage of persistence restore: recreates physics spaces from persisted world state. - * - *

Runs when {@code runtimeRestorePending} is true - * (set after Hytale deserializes the resource or after a manual snapshot load). For each - * persisted space definition, either creates a new runtime space or updates an existing - * one with the persisted gravity and settings.

- * - *

Persisted spaces bind to explicit backend ids. When a saved backend is not - * registered at restore time, the restore is treated as a hard failure and the - * persisted data is left untouched. That behavior keeps backend availability an - * explicit part of the persistence contract instead of silently changing physics - * behavior by falling back to a different backend.

- * - *

Runs before body and joint hydration so that target spaces exist when the - * downstream systems try to add bodies to them. Downstream systems declare - * {@code AFTER} this system in their dependency sets.

- */ -public class PersistentPhysicsSpaceBootstrapSystem extends TickingSystem { - - private static final ComponentType ATTACHMENT_TYPE = - PhysicsBodyAttachmentComponent.getComponentType(); - private static final ComponentType GENERATED_PROXY_TYPE = - GeneratedVisualProxyComponent.getComponentType(); - - private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); - - @Nonnull - private final SystemGroup group = ImpulsePlugin.get().getPersistenceRestoreGroup(); - - @Override - public void tick(float dt, int systemIndex, @Nonnull Store store) { - PersistentPhysicsWorldResource persistent = store.getResource( - PersistentPhysicsWorldResource.getResourceType()); - if (!persistent.isRuntimeRestorePending() - || persistent.isRuntimeSpaceBootstrapComplete() - || persistent.hasRuntimeRestoreFailed()) { - return; - } - - World world = store.getExternalData().getWorld(); - PhysicsWorldRuntimeResource runtime = PhysicsWorldRuntimeResource.require(store); - PersistentPhysicsSpaceState[] spaces = persistent.getSpaces(); - String validationFailure = PersistentPhysicsRestorePreflight.validate(persistent); - if (validationFailure != null) { - persistent.failRuntimeRestore(validationFailure); - LOGGER.at(Level.SEVERE).log(persistent.runtimeRestoreFailureSummary()); - return; - } - stripRuntimePhysicsStateForRestore(store, runtime, world); - try { - PhysicsOwnerBridge.run(store, "restore persisted physics runtime settings", - () -> runtime.setWorldSettings(persistent.getWorldSettings())); - } catch (RuntimeException exception) { - runtime.clearAllSpaces(world.getName()); - persistent.failRuntimeRestore("Invalid persisted physics runtime settings: " - + exception.getMessage()); - LOGGER.at(Level.SEVERE).log(persistent.runtimeRestoreFailureSummary()); - return; - } - - int restoredSpaceCount = 0; - for (PersistentPhysicsSpaceState state : spaces) { - SpaceId spaceId = state.toSpaceId(); - try { - PhysicsOwnerBridge.run(store, "bootstrap persisted physics space", () -> { - PhysicsSpaceBinding targetSpace = runtime.getSpaceBinding(spaceId); - if (targetSpace == null) { - runtime.createSpace(state.toBackendId(), - spaceId, - world.getName(), - state.toSettings()); - targetSpace = runtime.getSpaceBinding(spaceId); - } else { - runtime.setSpaceSettings(spaceId, state.toSettings()); - } - if (targetSpace == null) { - throw new IllegalStateException("Physics space id=" + spaceId - + " was not registered after creation"); - } - targetSpace.runtime().setGravity(targetSpace.backendSpaceHandle().value(), - state.getGravity().x, - state.getGravity().y, - state.getGravity().z); - }); - } catch (RuntimeException exception) { - runtime.clearAllSpaces(world.getName()); - persistent.failRuntimeRestore("Failed to bootstrap space id=" + state.getSpaceId() - + " backend=" + state.getBackendId() + ": " + exception.getMessage()); - LOGGER.at(Level.SEVERE).log( - "%s Cause: %s", - persistent.runtimeRestoreFailureSummary(), - exception.getMessage()); - return; - } - restoredSpaceCount++; - } - - persistent.markRuntimeSpaceBootstrapComplete(restoredSpaceCount); - } - - private static void stripRuntimePhysicsStateForRestore(@Nonnull Store store, - @Nonnull PhysicsWorldResource runtime, - @Nonnull World world) { - PhysicsOwnerBridge.run(store, "strip runtime physics state for restore", () -> { - runtime.clearAllSpaces(world.getName()); - runtime.clearBodies(); - }); - - store.forEachEntityParallel(ATTACHMENT_TYPE, - (index, archetypeChunk, commandBuffer) -> { - PhysicsBodyAttachmentComponent attachment = archetypeChunk.getComponent(index, - ATTACHMENT_TYPE); - if (attachment == null - || attachment.getLifecycle() != AttachmentLifecycle.GENERATED_PROXY) { - return; - } - - var ref = archetypeChunk.getReferenceTo(index); - commandBuffer.removeEntity(ref, RemoveReason.REMOVE); - }); - - store.forEachEntityParallel(GENERATED_PROXY_TYPE, - (index, archetypeChunk, commandBuffer) -> { - if (archetypeChunk.getComponent(index, ATTACHMENT_TYPE) != null) { - return; - } - - commandBuffer.removeEntity(archetypeChunk.getReferenceTo(index), RemoveReason.REMOVE); - }); - - if (PhysicsControlSessionComponent.isComponentTypeRegistered()) { - ComponentType controlSessionType = - PhysicsControlSessionComponent.getComponentType(); - store.forEachEntityParallel(controlSessionType, - (index, archetypeChunk, commandBuffer) -> commandBuffer.removeComponent( - archetypeChunk.getReferenceTo(index), - controlSessionType)); - } - } - - @Nonnull - @Override - public SystemGroup getGroup() { - return group; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsWorldSyncSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsWorldSyncSystem.java deleted file mode 100644 index a15832dd..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsWorldSyncSystem.java +++ /dev/null @@ -1,325 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.persistence; - -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.dependency.Dependency; -import com.hypixel.hytale.component.dependency.Order; -import com.hypixel.hytale.component.dependency.SystemDependency; -import com.hypixel.hytale.component.system.tick.TickingSystem; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsRuntimeSnapshot; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerResource; -import dev.hytalemodding.impulse.core.internal.systems.publication.PhysicsSnapshotPublicationSystem; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerBridge; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerSnapshot; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import java.util.Map; -import java.util.Set; -import java.util.WeakHashMap; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.RejectedExecutionException; -import javax.annotation.Nonnull; - -/** - * Syncs the persisted world resource from owner-lane runtime snapshots. - * - *

Mirrors the live spaces, their settings, persistent bodies, and persistent - * endpoint joints back into {@link PersistentPhysicsWorldResource} on a bounded - * cadence. Scalar world settings and cheap topology count changes request a full - * owner snapshot immediately; joint-only footprint checks are queued as light - * owner reads so the main tick does not inspect live backend joints.

- * - *

Runs after restore hydration to ensure both sides are settled before copying. - * Skipped while a restore is in progress, or after a hard restore failure, to avoid - * overwriting deserialized data before hydration finishes or before the failure is - * resolved.

- */ -public class PersistentPhysicsWorldSyncSystem extends TickingSystem { - - private static final int WORLD_SYNC_INTERVAL_TICKS = 20; - private static final Set> DEPENDENCIES = Set.of( - new SystemDependency<>(Order.AFTER, PersistentPhysicsJointHydrationSystem.class), - new SystemDependency<>(Order.AFTER, PhysicsSnapshotPublicationSystem.class) - ); - - private final Map pendingRuntimeReads = - new WeakHashMap<>(); - - @Override - public void tick(float dt, int systemIndex, @Nonnull Store store) { - PersistentPhysicsWorldResource persistent = store.getResource( - PersistentPhysicsWorldResource.getResourceType()); - if (shouldSkipRuntimeSnapshotTick(persistent)) { - pendingRuntimeReads.remove(persistent); - return; - } - - PhysicsWorldRuntimeResource runtime = PhysicsWorldRuntimeResource.require(store); - PendingRuntimeRead pending = pendingRuntimeReads.get(persistent); - if (pending != null) { - if (!pending.isDone()) { - return; - } - pendingRuntimeReads.remove(persistent); - if (pending.kind() == RuntimeReadKind.SNAPSHOT) { - syncRuntimeSnapshot(persistent, - pending.joinSnapshot(), - pending.restoreGeneration()); - return; - } - if (!pending.matchesGeneration(persistent)) { - return; - } - if (hasRuntimePersistenceFootprintChanged(persistent, pending.joinFootprint())) { - requestSnapshotRead(store, persistent, runtime); - } - return; - } - - if (!hasScalarWorldStateChanged(persistent, runtime) - && !hasCheapRuntimePersistenceFootprintChanged(persistent, runtime) - && !persistent.shouldSyncRuntimeSnapshot(WORLD_SYNC_INTERVAL_TICKS)) { - requestFootprintRead(store, persistent, runtime); - return; - } - - requestSnapshotRead(store, persistent, runtime); - } - - @Nonnull - public static SyncResult syncRuntimeSnapshot(@Nonnull Store store, - @Nonnull PersistentPhysicsWorldResource persistent, - @Nonnull PhysicsWorldResource runtime) { - if (persistent.isRuntimeRestorePending()) { - return SyncResult.skipped("restore pending"); - } - if (persistent.hasRuntimeRestoreFailed()) { - return SyncResult.skipped("restore failed"); - } - - PhysicsWorldRuntimeResource runtimeResource = PhysicsWorldRuntimeResource.require(runtime); - PersistentPhysicsRuntimeSnapshot snapshot = PhysicsOwnerBridge.call(store, - "capture persisted physics runtime snapshot", - () -> PersistentPhysicsRuntimeSnapshot.capture(runtimeResource)); - return syncRuntimeSnapshot(persistent, snapshot); - } - - @Nonnull - public static SyncResult syncRuntimeSnapshot(@Nonnull PersistentPhysicsWorldResource persistent, - @Nonnull PersistentPhysicsRuntimeSnapshot snapshot) { - return syncRuntimeSnapshot(persistent, - snapshot, - persistent.runtimeRestoreGeneration()); - } - - @Nonnull - static SyncResult syncRuntimeSnapshot(@Nonnull PersistentPhysicsWorldResource persistent, - @Nonnull PersistentPhysicsRuntimeSnapshot snapshot, - long restoreGeneration) { - if (persistent.runtimeRestoreGeneration() != restoreGeneration) { - return SyncResult.skipped("restore generation changed"); - } - if (persistent.isRuntimeRestorePending()) { - return SyncResult.skipped("restore pending"); - } - if (persistent.hasRuntimeRestoreFailed()) { - return SyncResult.skipped("restore failed"); - } - - persistent.setSchemaVersion(PersistentPhysicsWorldResource.CURRENT_SCHEMA_VERSION); - persistent.setWorldSettings(snapshot.getWorldSettings()); - persistent.setSpaces(snapshot.getSpaces()); - persistent.setBodies(snapshot.getBodies()); - persistent.setJoints(snapshot.getJoints()); - persistent.markRuntimeSnapshotSynced(); - - PersistentPhysicsRuntimeSnapshot.Footprint footprint = snapshot.getFootprint(); - return SyncResult.synced(footprint.spaces(), footprint.bodies(), footprint.joints()); - } - - static boolean shouldSkipRuntimeSnapshotTick( - @Nonnull PersistentPhysicsWorldResource persistent) { - return persistent.isRuntimeRestorePending() || persistent.hasRuntimeRestoreFailed(); - } - - private static boolean hasScalarWorldStateChanged(@Nonnull PersistentPhysicsWorldResource persistent, - @Nonnull PhysicsWorldRuntimeResource runtime) { - PhysicsWorldSettings runtimeSettings = runtime.getWorldSettings(); - PhysicsWorldSettings persistedSettings = persistent.getWorldSettings(); - return persistedSettings.getSimulationSteps() != runtimeSettings.getSimulationSteps() - || persistedSettings.getStepMode() != runtimeSettings.getStepMode() - || persistedSettings.getStepSchedulingMode() != runtimeSettings.getStepSchedulingMode() - || persistedSettings.getEventCollectionMode() != runtimeSettings.getEventCollectionMode() - || Float.compare(persistedSettings.getMaxStepDt(), runtimeSettings.getMaxStepDt()) != 0; - } - - private static boolean hasCheapRuntimePersistenceFootprintChanged( - @Nonnull PersistentPhysicsWorldResource persistent, - @Nonnull PhysicsWorldRuntimeResource runtime) { - return persistent.getSpaceCount() != runtime.getSpaceCount() - || persistent.getBodyCount() != runtime.getBodyRegistrationCount( - PhysicsBodyPersistenceMode.PERSISTENT); - } - - static boolean hasRuntimePersistenceFootprintChanged( - @Nonnull PersistentPhysicsWorldResource persistent, - @Nonnull PersistentPhysicsRuntimeSnapshot.Footprint footprint) { - return persistent.getSpaceCount() != footprint.spaces() - || persistent.getBodyCount() != footprint.bodies() - || persistent.getJointCount() != footprint.joints(); - } - - private void requestSnapshotRead(@Nonnull Store store, - @Nonnull PersistentPhysicsWorldResource persistent, - @Nonnull PhysicsWorldRuntimeResource runtime) { - if (runtime.canAccessLiveBackendDirectly()) { - syncRuntimeSnapshot(persistent, PersistentPhysicsRuntimeSnapshot.capture(runtime)); - return; - } - submitOwnerRead(store, - persistent, - PendingRuntimeRead.snapshot(persistent), - future -> future.complete(PersistentPhysicsRuntimeSnapshot.capture(runtime))); - } - - private void requestFootprintRead(@Nonnull Store store, - @Nonnull PersistentPhysicsWorldResource persistent, - @Nonnull PhysicsWorldRuntimeResource runtime) { - if (runtime.canAccessLiveBackendDirectly()) { - PersistentPhysicsRuntimeSnapshot.Footprint footprint = - PersistentPhysicsRuntimeSnapshot.captureFootprint(runtime); - if (hasRuntimePersistenceFootprintChanged(persistent, footprint)) { - requestSnapshotRead(store, persistent, runtime); - } - return; - } - submitOwnerRead(store, - persistent, - PendingRuntimeRead.footprint(persistent), - future -> future.complete(PersistentPhysicsRuntimeSnapshot.captureFootprint(runtime))); - } - - private void submitOwnerRead(@Nonnull Store store, - @Nonnull PersistentPhysicsWorldResource persistent, - @Nonnull PendingRuntimeRead pending, - @Nonnull OwnerReadCapture capture) { - PhysicsOwnerResource owner = store.getResource(PhysicsOwnerResource.getResourceType()); - if (owner.isClosed()) { - return; - } - - try { - owner.submitMutation(pending.operation(), () -> { - try { - capture.capture(pending.future()); - return PhysicsOwnerSnapshot.empty(); - } catch (RuntimeException | Error exception) { - pending.future().completeExceptionally(exception); - throw exception; - } - }); - pendingRuntimeReads.put(persistent, pending); - } catch (RejectedExecutionException exception) { - pending.future().completeExceptionally(exception); - } - } - - public record SyncResult(boolean synced, - int spaces, - int bodies, - int joints, - @Nonnull String skippedReason) { - - @Nonnull - private static SyncResult synced(int spaces, int bodies, int joints) { - return new SyncResult(true, spaces, bodies, joints, ""); - } - - @Nonnull - private static SyncResult skipped(@Nonnull String reason) { - return new SyncResult(false, 0, 0, 0, reason); - } - } - - private enum RuntimeReadKind { - SNAPSHOT, - FOOTPRINT - } - - private record PendingRuntimeRead(long restoreGeneration, - @Nonnull RuntimeReadKind kind, - @Nonnull CompletableFuture future) { - - @Nonnull - private static PendingRuntimeRead snapshot(@Nonnull PersistentPhysicsWorldResource persistent) { - return new PendingRuntimeRead(persistent.runtimeRestoreGeneration(), - RuntimeReadKind.SNAPSHOT, - new CompletableFuture<>()); - } - - @Nonnull - private static PendingRuntimeRead footprint(@Nonnull PersistentPhysicsWorldResource persistent) { - return new PendingRuntimeRead(persistent.runtimeRestoreGeneration(), - RuntimeReadKind.FOOTPRINT, - new CompletableFuture<>()); - } - - private boolean isDone() { - return future.isDone(); - } - - private boolean matchesGeneration(@Nonnull PersistentPhysicsWorldResource persistent) { - return restoreGeneration == persistent.runtimeRestoreGeneration(); - } - - @Nonnull - private String operation() { - return kind == RuntimeReadKind.SNAPSHOT - ? "capture persisted physics runtime snapshot" - : "capture persisted physics runtime footprint"; - } - - @Nonnull - private PersistentPhysicsRuntimeSnapshot joinSnapshot() { - return (PersistentPhysicsRuntimeSnapshot) joinFuture(); - } - - @Nonnull - private PersistentPhysicsRuntimeSnapshot.Footprint joinFootprint() { - return (PersistentPhysicsRuntimeSnapshot.Footprint) joinFuture(); - } - - @Nonnull - private Object joinFuture() { - try { - return future.join(); - } catch (CompletionException exception) { - Throwable cause = exception.getCause(); - if (cause instanceof RuntimeException runtimeException) { - throw runtimeException; - } - if (cause instanceof Error error) { - throw error; - } - throw exception; - } - } - } - - @FunctionalInterface - private interface OwnerReadCapture { - - void capture(@Nonnull CompletableFuture future); - } - - @Nonnull - @Override - public Set> getDependencies() { - return DEPENDENCIES; - } - -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PhysicsRuntimeHolderSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PhysicsRuntimeHolderSystem.java deleted file mode 100644 index f82d8050..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PhysicsRuntimeHolderSystem.java +++ /dev/null @@ -1,55 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.persistence; - -import com.hypixel.hytale.component.AddReason; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Holder; -import com.hypixel.hytale.component.RemoveReason; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.query.Query; -import com.hypixel.hytale.component.system.HolderSystem; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent.AttachmentLifecycle; -import javax.annotation.Nonnull; - -/** - * Strips runtime-only core physics components from holders when entities cross unload/load - * boundaries. - */ -public class PhysicsRuntimeHolderSystem extends HolderSystem { - - private static final ComponentType ATTACHMENT_TYPE = - PhysicsBodyAttachmentComponent.getComponentType(); - private static final Query QUERY = ATTACHMENT_TYPE; - - @Override - public void onEntityAdd(@Nonnull Holder holder, - @Nonnull AddReason reason, - @Nonnull Store store) { - if (reason == AddReason.LOAD) { - cleanupHolder(holder, store); - } - } - - @Override - public void onEntityRemoved(@Nonnull Holder holder, - @Nonnull RemoveReason reason, - @Nonnull Store store) { - cleanupHolder(holder, store); - } - - private static void cleanupHolder(@Nonnull Holder holder, - @Nonnull Store store) { - PhysicsBodyAttachmentComponent attachment = holder.getComponent(ATTACHMENT_TYPE); - if (attachment == null - || attachment.getLifecycle() == AttachmentLifecycle.GENERATED_PROXY) { - holder.tryRemoveComponent(ATTACHMENT_TYPE); - } - } - - @Nonnull - @Override - public Query getQuery() { - return QUERY; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/publication/CompletedStepPublicationSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/publication/CompletedStepPublicationSystem.java new file mode 100644 index 00000000..02514b9c --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/publication/CompletedStepPublicationSystem.java @@ -0,0 +1,88 @@ +package dev.hytalemodding.impulse.core.internal.systems.publication; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.QuerySystem; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsEventResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsProfilingResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStepSchedulerResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStepSchedulerResource.CompletedStep; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStepSchedulerResource.StepInput; +import dev.hytalemodding.impulse.core.internal.systems.PhysicsStoreSystemSupport; +import dev.hytalemodding.impulse.core.internal.systems.binding.TargetBindingSystem; +import java.util.Set; +import javax.annotation.Nonnull; + +/** + * Publishes the last completed backend state as a copied PhysicsStore snapshot frame. + */ +public final class CompletedStepPublicationSystem extends TickingSystem + implements QuerySystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, TargetBindingSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + CompletedStep completed = store.getResource(PhysicsStepSchedulerResource.getResourceType()) + .pollCompletedStep(); + if (completed == null) { + return; + } + if (completed.failed()) { + Throwable failure = completed.failure(); + String message = failure != null ? failure.getMessage() : null; + store.getResource(PhysicsRestoreStatusResource.getResourceType()) + .markFailed(message != null ? message : "PhysicsStore owner-lane step failed"); + throw new IllegalStateException("PhysicsStore owner-lane step failed", failure); + } + PhysicsSnapshotResource snapshot = store.getResource(PhysicsSnapshotResource.getResourceType()); + PhysicsProfilingResource profiling = store.getResource(PhysicsProfilingResource.getResourceType()); + profiling.recordStep(completed.stepSubmitNanos(), + completed.spaces(), + completed.substeps(), + completed.nativePhaseStats()); + StepInput input = completed.input(); + if (input != null) { + profiling.recordStepScheduling(input.inputDtSeconds(), + input.submittedDtSeconds(), + input.backlogDtSeconds(), + input.droppedBacklogDtSeconds(), + input.dtCapHit()); + } + PhysicsSnapshotResource.CompactSnapshot bodySnapshot = completed.bodySnapshot(); + long nextSequence = snapshot.latestSequence() + 1L; + float frameDt = input != null ? input.submittedDtSeconds() : dt; + snapshot.publish(nextSequence, frameDt, bodySnapshot); + int bodyCount = bodySnapshot.bodyCount(); + profiling.recordSnapshot(completed.snapshotNanos(), bodyCount); + store.getResource(PhysicsEventResource.getResourceType()) + .publishStepFrame(nextSequence, + Math.max(0L, store.getExternalData().getWorld().getTick()), + bodyCount, + profiling.getStepSubmitNanos(), + completed.snapshotNanos(), + completed.physicsEvents(), + completed.droppedBackendEventCount()); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + + @Nonnull + @Override + public Query getQuery() { + return PhysicsStoreSystemSupport.uuidQuery(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/publication/PhysicsPublicationPipeline.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/publication/PhysicsPublicationPipeline.java deleted file mode 100644 index 588d3930..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/publication/PhysicsPublicationPipeline.java +++ /dev/null @@ -1,110 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.publication; - -import com.hypixel.hytale.logger.HytaleLogger; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerMutationCompletion; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerResult; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerSnapshot; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerStepCompletion; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import java.util.List; -import java.util.logging.Level; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Names the frame boundary between owner execution and reader-visible physics state. - * - *

The publication system uses this class to drain completed mutations, apply one completed - * step snapshot, and write profiling data without blocking the entity-store tick.

- */ -public final class PhysicsPublicationPipeline { - - private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); - private static final int MAX_MUTATION_COMPLETIONS_PER_TICK = 64; - - private PhysicsPublicationPipeline() { - } - - public static int publishCompletedMutations(@Nonnull PhysicsOwnerResource owner) { - // Mutation completions report failures and clear handles; they do not publish snapshots. - List completions = - owner.pollCompletedMutations(MAX_MUTATION_COMPLETIONS_PER_TICK); - for (PhysicsOwnerMutationCompletion completion : completions) { - if (completion.executionFailure() != null) { - LOGGER.at(Level.SEVERE).log( - "Async physics owner-lane mutation failed while running %s: %s", - completion.operation(), - completion.executionFailure().getMessage()); - } - } - return completions.size(); - } - - @Nullable - public static PhysicsEventFrame publishCompletedStep(@Nonnull PhysicsOwnerResource owner, - @Nonnull PhysicsWorldResource resource, - @Nonnull PhysicsRuntimeProfilingResource profiling) { - return publishCompletedStep(owner, resource, profiling, 0L); - } - - @Nullable - public static PhysicsEventFrame publishCompletedStep(@Nonnull PhysicsOwnerResource owner, - @Nonnull PhysicsWorldResource resource, - @Nonnull PhysicsRuntimeProfilingResource profiling, - long publicationServerTick) { - PhysicsOwnerStepCompletion completion = owner.pollCompletedStep(); - if (completion == null) { - return null; - } - - if (completion.executionFailure() != null) { - LOGGER.at(Level.SEVERE).log("Async physics owner-lane step failed: %s", - completion.executionFailure().getMessage()); - return null; - } - - boolean currentFrame = true; - PublishedPhysicsSnapshotFrame frame = completion.frame(); - if (frame != null) { - // Reader-side apply happens here; command handles may have completed earlier. - PhysicsWorldRuntimeResource runtime = PhysicsWorldRuntimeResource.require(resource); - runtime.applyPublishedSnapshotFrame(frame, publicationServerTick); - if (frame.worldEpoch() != runtime.worldEpoch()) { - currentFrame = false; - } - } - - PhysicsOwnerSnapshot snapshot = completion.snapshotOrEmpty(); - if (profiling.isEnabled()) { - PhysicsOwnerResult result = completion.result(); - profiling.recordStep(snapshot.spaces(), - snapshot.substeps(), - snapshot.stepNanos(), - snapshot.bodySnapshots(), - snapshot.spatialIndexCells(), - snapshot.snapshotNanos(), - result != null ? result.queuedNanos() : 0L, - result != null ? result.runNanos() : 0L, - result != null ? result.completedNanos() : 0L, - snapshot.nativePhaseStats(), - completion.preStepDrainedMutations(), - completion.preStepDrainRunNanos(), - completion.lateMutationBacklogAtStep()); - } - - RuntimeException stepFailure = completion.stepFailure(); - if (stepFailure != null) { - LOGGER.at(Level.SEVERE).log( - "Async physics owner-lane step failed after %s completed substeps; snapshots were published with status=%s: %s", - snapshot.substeps(), - frame != null ? frame.status() : "", - stepFailure.getMessage()); - } - return currentFrame ? completion.eventFrame() : null; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/publication/PhysicsSnapshotPublicationSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/publication/PhysicsSnapshotPublicationSystem.java deleted file mode 100644 index dc947ec4..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/publication/PhysicsSnapshotPublicationSystem.java +++ /dev/null @@ -1,79 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.publication; - -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.dependency.Dependency; -import com.hypixel.hytale.component.dependency.Order; -import com.hypixel.hytale.component.dependency.SystemDependency; -import com.hypixel.hytale.component.system.tick.TickingSystem; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerResource; -import dev.hytalemodding.impulse.core.internal.systems.sync.PhysicsSyncSystem; -import dev.hytalemodding.impulse.core.internal.systems.visual.PhysicsDetachedVisualMaterializationSystem; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFramePublishedEvent; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Entity-tick publication stage for completed physics owner-lane output. - * - *

The command-buffer visibility chain is: recorded, queued, owner completed, snapshot captured, - * reader-side applied, then ECS systems consume the reader view. This system owns the reader-side - * apply step and intentionally never waits for an in-flight owner-lane step.

- */ -public final class PhysicsSnapshotPublicationSystem extends TickingSystem { - - private static final Set> DEPENDENCIES = Set.of( - new SystemDependency<>(Order.BEFORE, PhysicsDetachedVisualMaterializationSystem.class), - new SystemDependency<>(Order.BEFORE, PhysicsSyncSystem.class) - ); - - @Override - public void tick(float dt, int systemIndex, @Nonnull Store store) { - PhysicsOwnerResource owner = store.getResource( - PhysicsOwnerResource.getResourceType()); - if (owner.isClosed()) { - return; - } - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); - PhysicsRuntimeProfilingResource profiling = store.getResource( - PhysicsRuntimeProfilingResource.getResourceType()); - // Mutation futures are drained for failures/completion only; snapshot inclusion is still - // determined by the next captured and applied PublishedPhysicsSnapshotFrame. - PhysicsPublicationPipeline.publishCompletedMutations(owner); - long publicationServerTick = Math.max(0L, store.getExternalData().getWorld().getTick()); - PhysicsEventFrame frame = - PhysicsPublicationPipeline.publishCompletedStep(owner, resource, profiling, publicationServerTick); - if (frame != null) { - store.invoke(new PhysicsEventFramePublishedEvent(frame)); - } - } - - static int publishCompletedMutations(@Nonnull PhysicsOwnerResource owner) { - return PhysicsPublicationPipeline.publishCompletedMutations(owner); - } - - @Nullable - static PhysicsEventFrame publishCompletedStep(@Nonnull PhysicsOwnerResource owner, - @Nonnull PhysicsWorldResource resource, - @Nonnull PhysicsRuntimeProfilingResource profiling) { - return PhysicsPublicationPipeline.publishCompletedStep(owner, resource, profiling); - } - - @Nullable - static PhysicsEventFrame publishCompletedStep(@Nonnull PhysicsOwnerResource owner, - @Nonnull PhysicsWorldResource resource, - @Nonnull PhysicsRuntimeProfilingResource profiling, - long publicationServerTick) { - return PhysicsPublicationPipeline.publishCompletedStep(owner, resource, profiling, publicationServerTick); - } - - @Nonnull - @Override - public Set> getDependencies() { - return DEPENDENCIES; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/publication/PhysicsStoreEventPublicationSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/publication/PhysicsStoreEventPublicationSystem.java new file mode 100644 index 00000000..db327592 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/publication/PhysicsStoreEventPublicationSystem.java @@ -0,0 +1,105 @@ +package dev.hytalemodding.impulse.core.internal.systems.publication; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsEventResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsProfilingResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsProfilingResource.StepSample; +import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.sync.PhysicsSyncSystem; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.visual.PhysicsProjectionCleanupSystem; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFramePublishedEvent; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import javax.annotation.Nonnull; + +/** + * Forwards copied PhysicsStore event frames into the existing EntityStore world-event boundary. + */ +public final class PhysicsStoreEventPublicationSystem extends TickingSystem { + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.BEFORE, PhysicsProjectionCleanupSystem.class), + new SystemDependency<>(Order.BEFORE, PhysicsSyncSystem.class) + ); + + @Nonnull + private final Map, Long> lastPublishedSequences = + Collections.synchronizedMap(new WeakHashMap<>()); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + World world = store.getExternalData().getWorld(); + Store physics = PhysicsThreading.storeOrNull(world); + if (physics == null || physics.isShutdown()) { + return; + } + PhysicsThreading.requireWorldThread(physics, "publish PhysicsStore event frame"); + PhysicsEventFrame frame = physics.getResource(PhysicsEventResource.getResourceType()) + .getLatestFrame(); + if (frame.frameSequence() <= 0L || !markPublished(store, frame.frameSequence())) { + return; + } + recordProfiling(store, physics); + store.invoke(new PhysicsEventFramePublishedEvent(frame)); + } + + private boolean markPublished(@Nonnull Store store, long frameSequence) { + synchronized (lastPublishedSequences) { + long previous = lastPublishedSequences.getOrDefault(store, 0L); + if (frameSequence <= previous) { + return false; + } + lastPublishedSequences.put(store, frameSequence); + return true; + } + } + + private static void recordProfiling(@Nonnull Store store, + @Nonnull Store physics) { + assert PhysicsRuntimeProfilingResource.getResourceType() != null; + PhysicsRuntimeProfilingResource runtimeProfiling = store.getResource( + PhysicsRuntimeProfilingResource.getResourceType()); + if (!runtimeProfiling.isEnabled()) { + return; + } + StepSample sample = physics.getResource(PhysicsProfilingResource.getResourceType()) + .latestStepSample(); + runtimeProfiling.recordStep(sample.spaces(), + sample.substeps(), + sample.stepSubmitNanos(), + sample.publishedBodies(), + 0, + sample.snapshotNanos(), + 0L, + 0L, + System.nanoTime(), + sample.nativePhaseStats(), + 0, + 0L, + 0); + if (sample.schedulerSamples() > 0) { + runtimeProfiling.recordStepScheduling(sample.schedulerInputDtSeconds(), + sample.schedulerSubmittedDtSeconds(), + sample.schedulerBacklogDtSeconds(), + sample.droppedBacklogDtSeconds(), + sample.dtCapHit()); + } + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepCountPolicy.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepCountPolicy.java index 61db52be..57d59cff 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepCountPolicy.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepCountPolicy.java @@ -9,7 +9,7 @@ * *

Fixed-style modes use the configured count directly. Adaptive-style modes * treat it as a minimum and raise the count when the tick dt exceeds the - * configured max substep dt. The owner step command layers body-risk + * configured max substep dt. The store tick step pipeline layers body-risk * refinement on top for {@link PhysicsStepMode#ADAPTIVE}.

*/ public final class PhysicsStepCountPolicy { diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepRestoreGate.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepRestoreGate.java deleted file mode 100644 index cac27e3b..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepRestoreGate.java +++ /dev/null @@ -1,14 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.step; - -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import javax.annotation.Nonnull; - -final class PhysicsStepRestoreGate { - - private PhysicsStepRestoreGate() { - } - - static boolean canSubmitStep(@Nonnull PersistentPhysicsWorldResource persistent) { - return !persistent.isRuntimeRestorePending() && !persistent.hasRuntimeRestoreFailed(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepSystem.java deleted file mode 100644 index 98c9d73c..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepSystem.java +++ /dev/null @@ -1,369 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.step; - -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.system.tick.TickingSystem; -import com.hypixel.hytale.logger.HytaleLogger; -import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerStepCommand; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepSchedulingMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import java.util.Collections; -import java.util.Map; -import java.util.WeakHashMap; -import java.util.concurrent.RejectedExecutionException; -import java.util.function.BooleanSupplier; -import java.util.logging.Level; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Chunk-tick scheduler for physics steps. - * - *

This system only submits work to the per-world physics owner lane. It does not wait for step - * results or apply snapshots; {@code PhysicsSnapshotPublicationSystem} performs reader-side - * publication later on the entity-store tick.

- */ -public class PhysicsStepSystem extends TickingSystem implements AutoCloseable { - - private static final HytaleLogger LOGGER = HytaleLogger.get("Impulse"); - private static final float MAX_ACCUMULATED_STEP_DT = 0.25f; - - @Nonnull - private final Map, StepSchedulerState> statesByStore = - Collections.synchronizedMap(new WeakHashMap<>()); - private volatile boolean closed; - - public PhysicsStepSystem() { - } - - @Override - public void close() { - closed = true; - } - - @Override - public void tick(float dt, int systemIndex, @Nonnull Store store) { - if (closed) { - return; - } - - var world = store.getExternalData().getWorld(); - var entityStore = world.getEntityStore().getStore(); - PhysicsWorldResource resource = entityStore.getResource( - PhysicsWorldResource.getResourceType()); - PhysicsRuntimeProfilingResource profiling = entityStore.getResource( - PhysicsRuntimeProfilingResource.getResourceType()); - PhysicsOwnerResource owner = entityStore.getResource( - PhysicsOwnerResource.getResourceType()); - if (owner.isClosed()) { - return; - } - PersistentPhysicsWorldResource persistent = entityStore.getResource( - PersistentPhysicsWorldResource.getResourceType()); - - submitStepIfRestoreReady(stateFor(store), - persistent, - owner, - resource, - dt, - profiling, - Math.max(0L, world.getTick())); - } - - boolean submitStepIfRestoreReady(@Nonnull StepSchedulerState state, - @Nonnull PersistentPhysicsWorldResource persistent, - @Nonnull PhysicsOwnerResource owner, - @Nonnull PhysicsWorldResource resource, - float dt, - @Nonnull PhysicsRuntimeProfilingResource profiling, - long serverTick) { - if (!PhysicsStepRestoreGate.canSubmitStep(persistent)) { - /* - * Restore replaces runtime topology. Dropping catch-up dt avoids replaying stale - * backlog against newly restored spaces after restore completes. - */ - state.clearAccumulatedStepDt(); - return false; - } - submitStepIfIdle(state, owner, resource, dt, profiling, serverTick); - return true; - } - - void submitStepIfIdle(@Nonnull StepSchedulerState state, - @Nonnull PhysicsOwnerResource owner, - @Nonnull PhysicsWorldResource resource, - float dt, - @Nonnull PhysicsRuntimeProfilingResource profiling, - long serverTick) { - PhysicsWorldSettings settings = resource.getWorldSettings(); - if (settings.getStepSchedulingMode() - != PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT) { - submitCurrentTickStepIfIdle(state, owner, resource, dt, profiling, serverTick); - return; - } - - submitAccumulatedStepIfIdle(state, owner, resource, dt, profiling, serverTick); - } - - private void submitCurrentTickStepIfIdle(@Nonnull StepSchedulerState state, - @Nonnull PhysicsOwnerResource owner, - @Nonnull PhysicsWorldResource resource, - float dt, - @Nonnull PhysicsRuntimeProfilingResource profiling, - long serverTick) { - boolean profilingEnabled = profiling.isEnabled(); - state.clearAccumulatedStepDt(); - if (owner.hasPendingStep()) { - if (profilingEnabled) { - profiling.recordStepSkippedPending(owner.pendingStepAgeNanos()); - } - return; - } - - PhysicsOwnerStepCommand command = state.currentTickStepCommand(resource, - dt, - profilingEnabled, - serverTick); - try { - if (owner.submitStepIfIdle(command)) { - state.consumeStepSequence(); - return; - } - if (profilingEnabled) { - profiling.recordStepSkippedPending(owner.pendingStepAgeNanos()); - } - } catch (RejectedExecutionException exception) { - if (!owner.isClosed()) { - LOGGER.at(Level.WARNING).log( - "Skipping async physics step because the owner lane is unavailable: %s", - exception.getMessage()); - } - } - } - - private void submitAccumulatedStepIfIdle(@Nonnull StepSchedulerState state, - @Nonnull PhysicsOwnerResource owner, - @Nonnull PhysicsWorldResource resource, - float dt, - @Nonnull PhysicsRuntimeProfilingResource profiling, - long serverTick) { - boolean profilingEnabled = profiling.isEnabled(); - float safeDt = safeStepDt(dt); - float maxAccumulatedDt = maxAccumulatedStepDt(resource); - AccumulatedStepPreparation preparation = state.prepareAccumulatedStepCommand(safeDt, - maxAccumulatedDt, - owner::hasPendingStep, - resource, - profilingEnabled, - serverTick); - StepSchedulerSample sample = preparation.sample(); - if (preparation.hasPendingStep()) { - recordSchedulerSample(profiling, - profilingEnabled, - safeDt, - 0.0f, - preparation.backlogDt(), - sample.droppedDt(), - sample.capHit()); - if (profilingEnabled) { - profiling.recordStepSkippedPending(owner.pendingStepAgeNanos()); - } - return; - } - PhysicsOwnerStepCommand command = preparation.commandOrThrow(); - try { - if (owner.submitStepIfIdle(command)) { - float submittedDt = state.consumeSubmittedStepDt(); - recordSchedulerSample(profiling, - profilingEnabled, - safeDt, - submittedDt, - 0.0f, - sample.droppedDt(), - sample.capHit()); - return; - } - recordSchedulerSample(profiling, - profilingEnabled, - safeDt, - 0.0f, - state.accumulatedStepDt(), - sample.droppedDt(), - sample.capHit()); - if (profilingEnabled) { - profiling.recordStepSkippedPending(owner.pendingStepAgeNanos()); - } - } catch (RejectedExecutionException exception) { - recordSchedulerSample(profiling, - profilingEnabled, - safeDt, - 0.0f, - state.accumulatedStepDt(), - sample.droppedDt(), - sample.capHit()); - if (!owner.isClosed()) { - LOGGER.at(Level.WARNING).log( - "Skipping async physics step because the owner lane is unavailable: %s", - exception.getMessage()); - } - } - } - - @Nonnull - private StepSchedulerState stateFor(@Nonnull Store store) { - synchronized (statesByStore) { - return statesByStore.computeIfAbsent(store, _ -> new StepSchedulerState()); - } - } - - private static void recordSchedulerSample(@Nonnull PhysicsRuntimeProfilingResource profiling, - boolean profilingEnabled, - float inputDt, - float submittedDt, - float backlogDt, - float droppedDt, - boolean capHit) { - if (profilingEnabled) { - profiling.recordStepScheduling(inputDt, submittedDt, backlogDt, droppedDt, capHit); - } - } - - private static float safeStepDt(float dt) { - return Float.isFinite(dt) ? Math.max(dt, 0.0f) : 0.0f; - } - - static float maxAccumulatedStepDt(@Nonnull PhysicsWorldResource resource) { - PhysicsWorldSettings settings = resource.getWorldSettings(); - float maxStepDt = settings.getMaxStepDt(); - float safeMaxStepDt = Float.isFinite(maxStepDt) && maxStepDt > 0.0f - ? maxStepDt - : PhysicsWorldSettings.DEFAULT_MAX_STEP_DT; - /* - * Fixed and CCD modes do not refine catch-up dt inside the owner lane: they - * use the configured substep count directly. Cap accumulated submissions - * to that exact budget and drop the rest through the scheduler profiling - * path. Adaptive/progressive modes keep the wider refinement window. - */ - int stepBudget = switch (settings.getStepMode()) { - case FIXED, CCD -> Math.clamp(settings.getSimulationSteps(), - PhysicsWorldSettings.MIN_SIMULATION_STEPS, - PhysicsWorldSettings.MAX_SIMULATION_STEPS); - case ADAPTIVE, PROGRESSIVE_REFINEMENT -> PhysicsWorldSettings.MAX_SIMULATION_STEPS; - }; - return Math.min(MAX_ACCUMULATED_STEP_DT, safeMaxStepDt * stepBudget); - } - - static final class StepSchedulerState { - - private final Object lock = new Object(); - - /** - * Impulse-local sequence for correlating owner step commands with published - * snapshot frames. This is not the Hytale world tick. - */ - private long nextStepSequence = 1L; - - /* - * Accumulated scheduler dt is intentionally local to this ChunkStore scheduler. It is - * submitted only when the owner lane has no in-flight step. - */ - private double accumulatedStepDt; - - @Nonnull - private AccumulatedStepPreparation prepareAccumulatedStepCommand(float dt, - float maxAccumulatedDt, - @Nonnull BooleanSupplier hasPendingStep, - @Nonnull PhysicsWorldResource resource, - boolean profilingEnabled, - long serverTick) { - synchronized (lock) { - StepSchedulerSample sample = accumulateLocked(dt, maxAccumulatedDt); - if (hasPendingStep.getAsBoolean()) { - return new AccumulatedStepPreparation(sample, - (float) accumulatedStepDt, - null); - } - PhysicsOwnerStepCommand command = new PhysicsOwnerStepCommand(resource, - (float) accumulatedStepDt, - profilingEnabled, - nextStepSequence, - serverTick); - return new AccumulatedStepPreparation(sample, 0.0f, command); - } - } - - @Nonnull - PhysicsOwnerStepCommand currentTickStepCommand(@Nonnull PhysicsWorldResource resource, - float dt, - boolean profilingEnabled, - long serverTick) { - synchronized (lock) { - return new PhysicsOwnerStepCommand(resource, - dt, - profilingEnabled, - nextStepSequence, - serverTick); - } - } - - @Nonnull - private StepSchedulerSample accumulateLocked(float dt, float maxAccumulatedDt) { - double candidate = accumulatedStepDt + Math.max(0.0f, dt); - double capped = Math.clamp(maxAccumulatedDt, 0.0f, candidate); - accumulatedStepDt = capped; - double dropped = Math.max(0.0, candidate - capped); - return new StepSchedulerSample((float) dropped, dropped > 0.0); - } - - private float consumeSubmittedStepDt() { - synchronized (lock) { - float submittedDt = (float) accumulatedStepDt; - accumulatedStepDt = 0.0; - nextStepSequence++; - return submittedDt; - } - } - - private void clearAccumulatedStepDt() { - synchronized (lock) { - accumulatedStepDt = 0.0; - } - } - - private void consumeStepSequence() { - synchronized (lock) { - nextStepSequence++; - } - } - - private float accumulatedStepDt() { - synchronized (lock) { - return (float) accumulatedStepDt; - } - } - } - - private record AccumulatedStepPreparation(@Nonnull StepSchedulerSample sample, - float backlogDt, - @Nullable PhysicsOwnerStepCommand command) { - - private boolean hasPendingStep() { - return command == null; - } - - @Nonnull - private PhysicsOwnerStepCommand commandOrThrow() { - if (command == null) { - throw new IllegalStateException("No accumulated step command was prepared"); - } - return command; - } - } - - private record StepSchedulerSample(float droppedDt, boolean capHit) { - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/step/StepSubmissionSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/step/StepSubmissionSystem.java new file mode 100644 index 00000000..3e3e4bef --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/step/StepSubmissionSystem.java @@ -0,0 +1,794 @@ +package dev.hytalemodding.impulse.core.internal.systems.step; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.dependency.Dependency; +import com.hypixel.hytale.component.dependency.Order; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.system.tick.TickingSystem; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.PhysicsContactPhase; +import dev.hytalemodding.impulse.api.PhysicsStepPhaseStats; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendBodySnapshotSink; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.runtime.BackendStepPhaseStatsSink; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsProfilingResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource.BodyHitMetadata; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource.BodySnapshotMetadata; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStepSchedulerResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStepSchedulerResource.CompletedStep; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStepSchedulerResource.StepInput; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldSettingsResource; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.systems.PhysicsStoreQueuedReadSystem; +import dev.hytalemodding.impulse.core.internal.systems.PhysicsStoreSystemSupport; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsContactEvent; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsFrameEvent; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Vector3f; + +/** + * Submits the next backend step from PhysicsStore.tick(). + */ +public final class StepSubmissionSystem extends TickingSystem { + + private static final float DEFAULT_LINEAR_TRAVEL_PER_SUBSTEP = 0.45f; + private static final float MIN_LINEAR_TRAVEL_PER_SUBSTEP = 0.125f; + private static final float SHAPE_TRAVEL_FRACTION = 0.75f; + private static final float MAX_ANGULAR_RADIANS_PER_SUBSTEP = (float) Math.toRadians(30.0); + + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, PhysicsStoreQueuedReadSystem.class) + ); + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + if (restore.isPending() || restore.isFailed()) { + return; + } + float safeDt = Float.isFinite(dt) ? Math.max(dt, 0.0f) : 0.0f; + if (safeDt <= 0.0f) { + return; + } + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + List bindings = runtimeStepBindings(runtime); + if (bindings.isEmpty()) { + return; + } + + PhysicsWorldSettingsResource settingsResource = store.getResource( + PhysicsWorldSettingsResource.getResourceType()); + PhysicsWorldSettings settings = settingsResource.getSettings(); + PhysicsSpaceCompatibilityIndexResource compatibility = store.getResource( + PhysicsSpaceCompatibilityIndexResource.getResourceType()); + PhysicsStepSchedulerResource scheduler = store.getResource( + PhysicsStepSchedulerResource.getResourceType()); + StepInput input = scheduler.acceptStepInput(safeDt, + settings.getStepSchedulingMode(), + maxSubmittedDtSeconds(settings)); + float submittedDt = input.submittedDtSeconds(); + if (submittedDt <= 0.0f) { + return; + } + PhysicsStepMode stepMode = settings.getStepMode(); + float maxStepDt = settings.getMaxStepDt() > 0.0f + ? settings.getMaxStepDt() + : PhysicsWorldSettings.DEFAULT_MAX_STEP_DT; + int steps = stepMode == PhysicsStepMode.ADAPTIVE + ? resolveAdaptiveStepCount(runtime, submittedDt, settings.getSimulationSteps(), maxStepDt) + : PhysicsStepCountPolicy.resolveStepCount(submittedDt, + settings.getSimulationSteps(), + maxStepDt, + stepMode); + float stepDt = submittedDt / steps; + boolean ccdMode = stepMode == PhysicsStepMode.CCD; + if (ccdMode || settingsResource.isCcdStepModeActive()) { + syncContinuousCollisionMode(store, runtime, ccdMode); + } + settingsResource.setCcdStepModeActive(ccdMode); + PhysicsProfilingResource profiling = store.getResource(PhysicsProfilingResource.getResourceType()); + boolean profilingEnabled = profiling.isEnabled(); + if (profilingEnabled) { + resetStepPhaseStats(runtime); + } + boolean collectBackendEvents = settings.getEventCollectionMode().collectsBackendEvents(); + boolean submitted = scheduler.submitStep(input, + () -> runOwnerStep(runtime, + compatibility, + bindings, + steps, + stepDt, + profilingEnabled, + collectBackendEvents), + System.nanoTime()); + if (!submitted) { + throw new IllegalStateException("PhysicsStore owner-lane scheduler refused a submitted step"); + } + } + + @Nonnull + private static CompletedStep runOwnerStep(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsSpaceCompatibilityIndexResource compatibility, + @Nonnull List bindings, + int steps, + float stepDt, + boolean profilingEnabled, + boolean collectBackendEvents) { + long stepStartNanos = profilingEnabled ? System.nanoTime() : 0L; + StepCounters counters = new StepCounters(); + for (RuntimeStepBinding binding : bindings) { + counters.spaceCount++; + for (int step = 0; step < steps; step++) { + //noinspection resource + binding.backendRuntime().step(binding.spaceHandle().value(), stepDt); + counters.substeps++; + } + } + long stepNanos = profilingEnabled ? System.nanoTime() - stepStartNanos : 0L; + PhysicsStepPhaseStats nativePhaseStats = profilingEnabled + ? collectStepPhaseStats(bindings) + : PhysicsStepPhaseStats.unavailable(); + long snapshotStartNanos = profilingEnabled ? System.nanoTime() : 0L; + PhysicsSnapshotResource.CompactSnapshot bodySnapshot = collectOwnerLaneSnapshot(runtime, + bindings); + long snapshotNanos = profilingEnabled ? System.nanoTime() - snapshotStartNanos : 0L; + StepBackendEvents backendEvents = collectOwnerLaneBackendEvents(runtime, + compatibility, + bindings, + collectBackendEvents); + return new CompletedStep(counters.spaceCount, + counters.substeps, + stepNanos, + snapshotNanos, + nativePhaseStats, + bodySnapshot, + backendEvents.physicsEvents(), + backendEvents.droppedBackendEventCount()); + } + + @Nonnull + private static List runtimeStepBindings( + @Nonnull PhysicsRuntimeResource runtime) { + List bindings = new ArrayList<>(); + runtime.forEachRuntimeSpaceBinding((spaceRef, backendId, spaceHandle, backendRuntime) -> + bindings.add(new RuntimeStepBinding(spaceRef, backendId, spaceHandle, backendRuntime))); + return bindings; + } + + @Nonnull + private static PhysicsSnapshotResource.CompactSnapshot collectOwnerLaneSnapshot( + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull List bindings) { + PhysicsSnapshotResource.CompactSnapshotBuilder snapshot = + PhysicsSnapshotResource.compactBuilder(runtimeBodyHandleCount(runtime, bindings)); + for (RuntimeStepBinding binding : bindings) { + //noinspection resource + binding.backendRuntime().snapshotBodies(binding.spaceHandle().value(), + bodyIds -> runtime.forEachBodyHandle(binding.backendId(), + binding.spaceHandle(), + bodyIds), + (bodyId, + _, + bodyTypeCode, + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW, + linearVelocityX, + linearVelocityY, + linearVelocityZ, + angularVelocityX, + angularVelocityY, + angularVelocityZ, + sleeping, + _, + _, + _, + _, + _, + _, + _, + _, + _, + centerOfMassOffsetY, + _, + _, + _, + _, + _, + _, + _) -> collectOwnerLaneSnapshot(runtime, + snapshot, + binding.backendId(), + binding.spaceHandle(), + bodyId, + bodyTypeCode, + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW, + linearVelocityX, + linearVelocityY, + linearVelocityZ, + angularVelocityX, + angularVelocityY, + angularVelocityZ, + centerOfMassOffsetY, + sleeping)); + } + return snapshot.build(); + } + + private static int runtimeBodyHandleCount(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull List bindings) { + int bodyCount = 0; + for (RuntimeStepBinding binding : bindings) { + bodyCount += runtime.bodyHandleCount(binding.backendId(), binding.spaceHandle()); + } + return bodyCount; + } + + private static void collectOwnerLaneSnapshot(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsSnapshotResource.CompactSnapshotBuilder snapshot, + @Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + long bodyId, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW, + float linearVelocityX, + float linearVelocityY, + float linearVelocityZ, + float angularVelocityX, + float angularVelocityY, + float angularVelocityZ, + float centerOfMassOffsetY, + boolean sleeping) { + BodySnapshotMetadata metadata = runtime.getBodySnapshotMetadata(backendId, + spaceHandle, + bodyId); + if (metadata == null) { + return; + } + snapshot.addBody(metadata.bodyRef(), + metadata.bodyUuid(), + metadata.spaceUuid(), + BackendRuntimeCodes.bodyType(bodyTypeCode), + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW, + linearVelocityX, + linearVelocityY, + linearVelocityZ, + angularVelocityX, + angularVelocityY, + angularVelocityZ, + centerOfMassOffsetY, + sleeping); + } + + @SuppressWarnings("resource") + @Nonnull + private static StepBackendEvents collectOwnerLaneBackendEvents( + @Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsSpaceCompatibilityIndexResource compatibility, + @Nonnull List bindings, + boolean collectBackendEvents) { + if (!collectBackendEvents) { + return StepBackendEvents.EMPTY; + } + StepBackendEvents backendEvents = new StepBackendEvents(); + for (RuntimeStepBinding binding : bindings) { + UUID spaceUuid = runtime.getSpaceUuid(binding.spaceRef()); + if (spaceUuid == null) { + backendEvents.addDropped( + binding.backendRuntime().contactCount(binding.spaceHandle().value())); + continue; + } + SpaceId spaceId = compatibility.getSpaceId(spaceUuid); + if (spaceId == null) { + backendEvents.addDropped( + binding.backendRuntime().contactCount(binding.spaceHandle().value())); + continue; + } + binding.backendRuntime().contacts(binding.spaceHandle().value(), (bodyAId, + bodyBId, + pointAX, + pointAY, + pointAZ, + pointBX, + pointBY, + pointBZ, + normalBX, + normalBY, + normalBZ, + distance, + impulse) -> collectOwnerLaneContactEvent(runtime, + backendEvents, + binding.backendId(), + binding.spaceHandle(), + spaceId, + bodyAId, + bodyBId, + pointAX, + pointAY, + pointAZ, + pointBX, + pointBY, + pointBZ, + normalBX, + normalBY, + normalBZ, + distance, + impulse)); + } + return backendEvents; + } + + private static void collectOwnerLaneContactEvent(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull StepBackendEvents backendEvents, + @Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull SpaceId spaceId, + long bodyAId, + long bodyBId, + float pointAX, + float pointAY, + float pointAZ, + float pointBX, + float pointBY, + float pointBZ, + float normalBX, + float normalBY, + float normalBZ, + float distance, + float impulse) { + BodyHitMetadata bodyA = runtime.getBodyHitMetadata(backendId, spaceHandle, bodyAId); + BodyHitMetadata bodyB = runtime.getBodyHitMetadata(backendId, spaceHandle, bodyBId); + if (bodyA == null + || bodyA.bodyRef() == null + || bodyB == null + || bodyB.bodyRef() == null + || PhysicsStoreSystemSupport.isNil(bodyA.bodyUuid()) + || PhysicsStoreSystemSupport.isNil(bodyB.bodyUuid())) { + backendEvents.addDropped(1); + return; + } + backendEvents.add(new PhysicsContactEvent(spaceId, + PhysicsContactPhase.OBSERVED, + bodyA.bodyUuid(), + bodyB.bodyUuid(), + new Vector3f(pointAX, pointAY, pointAZ), + new Vector3f(pointBX, pointBY, pointBZ), + new Vector3f(normalBX, normalBY, normalBZ), + distance, + impulse)); + } + + private static int resolveAdaptiveStepCount(@Nonnull PhysicsRuntimeResource runtime, + float dt, + int simulationSteps, + float maxStepDt) { + int minimumSteps = PhysicsStepCountPolicy.resolveMaxStepCount(dt, + simulationSteps, + maxStepDt); + StepRisk risk = new StepRisk(dt, minimumSteps); + runtime.forEachRuntimeSpaceBinding((_, backendId, spaceHandle, backendRuntime) -> + backendRuntime.snapshotBodies(spaceHandle.value(), + bodyIds -> runtime.forEachBodyHandle(backendId, spaceHandle, bodyIds), + risk)); + return risk.steps(); + } + + private static float maxSubmittedDtSeconds(@Nonnull PhysicsWorldSettings settings) { + float maxStepDt = settings.getMaxStepDt() > 0.0f + ? settings.getMaxStepDt() + : PhysicsWorldSettings.DEFAULT_MAX_STEP_DT; + int maxSteps = switch (settings.getStepMode()) { + case FIXED, CCD -> settings.getSimulationSteps(); + case ADAPTIVE, PROGRESSIVE_REFINEMENT -> PhysicsWorldSettings.MAX_SIMULATION_STEPS; + }; + return maxStepDt * maxSteps; + } + + private static void syncContinuousCollisionMode(@Nonnull Store store, + @Nonnull PhysicsRuntimeResource runtime, + boolean forceDynamicBodies) { + runtime.forEachRuntimeSpaceBinding((_, backendId, spaceHandle, backendRuntime) -> { + if (!backendRuntime.supportsContinuousCollision(spaceHandle.value())) { + return; + } + runtime.forEachBodyHandle(backendId, spaceHandle, bodyId -> { + BodySnapshotMetadata metadata = runtime.getBodySnapshotMetadata(backendId, + spaceHandle, + bodyId); + boolean authoredCcd = metadata != null + && authoredContinuousCollision(store, metadata); + backendRuntime.bodySnapshot(spaceHandle.value(), + bodyId, + new ContinuousCollisionSync(backendRuntime, + spaceHandle, + bodyId, + forceDynamicBodies || authoredCcd)); + }); + }); + } + + private static boolean authoredContinuousCollision(@Nonnull Store store, + @Nonnull BodySnapshotMetadata metadata) { + DynamicsComponent dynamics = PhysicsStoreSystemSupport.component(store, + metadata.bodyRef(), + DynamicsComponent.getComponentType()); + return dynamics != null && dynamics.isContinuousCollisionEnabled(); + } + + private static void resetStepPhaseStats(@Nonnull PhysicsRuntimeResource runtime) { + runtime.forEachRuntimeSpaceBinding((_, _, spaceHandle, backendRuntime) -> + backendRuntime.resetStepPhaseStats(spaceHandle.value())); + } + + @Nonnull + private static PhysicsStepPhaseStats collectStepPhaseStats( + @Nonnull List bindings) { + StepPhaseStatsAccumulator stats = new StepPhaseStatsAccumulator(); + StepPhaseStatsCapture capture = new StepPhaseStatsCapture(); + for (RuntimeStepBinding binding : bindings) { + capture.reset(); + //noinspection resource + binding.backendRuntime().stepPhaseStats(binding.spaceHandle().value(), capture); + stats.add(capture.value()); + } + return stats.value(); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + + private record ContinuousCollisionSync(@Nonnull PhysicsBackendRuntime backendRuntime, + @Nonnull BackendSpaceHandle spaceHandle, long bodyId, + boolean targetEnabled) implements + BackendBodySnapshotSink { + + @Override + public void accept(long bodyId, + int shapeTypeCode, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW, + float linearVelocityX, + float linearVelocityY, + float linearVelocityZ, + float angularVelocityX, + float angularVelocityY, + float angularVelocityZ, + boolean sleeping, + boolean sensor, + float mass, + float friction, + float restitution, + float linearDamping, + float angularDamping, + int collisionGroup, + int collisionMask, + boolean continuousCollisionEnabled, + float centerOfMassOffsetY, + boolean hasBoxHalfExtents, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode) { + if (BackendRuntimeCodes.bodyType(bodyTypeCode) != PhysicsBodyType.DYNAMIC + || continuousCollisionEnabled == targetEnabled) { + return; + } + backendRuntime.setBodyContinuousCollision(spaceHandle.value(), + this.bodyId, + targetEnabled); + } + } + + private static final class StepRisk implements BackendBodySnapshotSink { + + private final float dt; + private int steps; + + private StepRisk(float dt, int minimumSteps) { + this.dt = dt; + steps = minimumSteps; + } + + @Override + public void accept(long bodyId, + int shapeTypeCode, + int bodyTypeCode, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW, + float linearVelocityX, + float linearVelocityY, + float linearVelocityZ, + float angularVelocityX, + float angularVelocityY, + float angularVelocityZ, + boolean sleeping, + boolean sensor, + float mass, + float friction, + float restitution, + float linearDamping, + float angularDamping, + int collisionGroup, + int collisionMask, + boolean continuousCollisionEnabled, + float centerOfMassOffsetY, + boolean hasBoxHalfExtents, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight, + int axisCode) { + if (steps >= PhysicsWorldSettings.MAX_SIMULATION_STEPS + || sleeping + || sensor) { + return; + } + PhysicsBodyType bodyType = BackendRuntimeCodes.bodyType(bodyTypeCode); + if (bodyType != PhysicsBodyType.DYNAMIC && bodyType != PhysicsBodyType.KINEMATIC) { + return; + } + + ShapeType shapeType = BackendRuntimeCodes.shapeType(shapeTypeCode); + float linearTravel = vectorLength(linearVelocityX, + linearVelocityY, + linearVelocityZ) * dt; + float shapeRadius = approximateShapeRadius(shapeType, + hasBoxHalfExtents, + halfExtentX, + halfExtentY, + halfExtentZ, + radius, + halfHeight); + float angularSurfaceTravel = vectorLength(angularVelocityX, + angularVelocityY, + angularVelocityZ) * shapeRadius * dt; + int requiredSteps = Math.max( + requiredSteps(linearTravel, + safeLinearTravel(shapeType, + hasBoxHalfExtents, + halfExtentX, + halfExtentY, + halfExtentZ, + radius)), + requiredSteps(angularSurfaceTravel, safeAngularTravel(shapeRadius))); + steps = Math.clamp(steps, + Math.min(requiredSteps, PhysicsWorldSettings.MAX_SIMULATION_STEPS), + PhysicsWorldSettings.MAX_SIMULATION_STEPS); + } + + private int steps() { + return steps; + } + } + + private static final class StepPhaseStatsCapture implements BackendStepPhaseStatsSink { + + @Nonnull + private PhysicsStepPhaseStats value = PhysicsStepPhaseStats.unavailable(); + + @Override + public void accept(long stepNanos, + long broadPhaseNanos, + long narrowPhaseNanos, + long solverNanos, + long continuousCollisionNanos, + long snapshotNanos, + boolean available) { + value = available + ? PhysicsStepPhaseStats.available(stepNanos, + broadPhaseNanos, + narrowPhaseNanos, + solverNanos, + continuousCollisionNanos, + snapshotNanos) + : PhysicsStepPhaseStats.unavailable(); + } + + private void reset() { + value = PhysicsStepPhaseStats.unavailable(); + } + + @Nonnull + private PhysicsStepPhaseStats value() { + return value; + } + } + + private static final class StepPhaseStatsAccumulator { + + @Nonnull + private PhysicsStepPhaseStats value = PhysicsStepPhaseStats.unavailable(); + + private void add(@Nonnull PhysicsStepPhaseStats stats) { + value = value.add(stats); + } + + @Nonnull + private PhysicsStepPhaseStats value() { + return value; + } + } + + private static final class StepCounters { + + private int spaceCount; + private int substeps; + } + + private record RuntimeStepBinding(@Nonnull Ref spaceRef, + @Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull PhysicsBackendRuntime backendRuntime) { + } + + private static final class StepBackendEvents { + + @Nonnull + private static final StepBackendEvents EMPTY = new StepBackendEvents(List.of(), 0); + + @Nonnull + private final List physicsEvents; + private int droppedBackendEventCount; + + private StepBackendEvents() { + this(new ArrayList<>(), 0); + } + + private StepBackendEvents(@Nonnull List physicsEvents, + int droppedBackendEventCount) { + this.physicsEvents = physicsEvents; + this.droppedBackendEventCount = Math.max(0, droppedBackendEventCount); + } + + private void add(@Nonnull PhysicsFrameEvent event) { + physicsEvents.add(event); + } + + private void addDropped(int count) { + droppedBackendEventCount += Math.max(0, count); + } + + @Nonnull + private List physicsEvents() { + return physicsEvents; + } + + private int droppedBackendEventCount() { + return droppedBackendEventCount; + } + } + + private static int requiredSteps(float travel, float safeTravel) { + if (travel <= safeTravel) { + return 1; + } + return (int) Math.ceil(travel / safeTravel); + } + + private static float safeLinearTravel(@Nonnull ShapeType shapeType, + boolean hasBoxHalfExtents, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius) { + return Math.clamp( + approximateMinimumExtent(shapeType, + hasBoxHalfExtents, + halfExtentX, + halfExtentY, + halfExtentZ, + radius) * SHAPE_TRAVEL_FRACTION, + MIN_LINEAR_TRAVEL_PER_SUBSTEP, + DEFAULT_LINEAR_TRAVEL_PER_SUBSTEP); + } + + private static float safeAngularTravel(float shapeRadius) { + return Math.clamp(shapeRadius * MAX_ANGULAR_RADIANS_PER_SUBSTEP, + MIN_LINEAR_TRAVEL_PER_SUBSTEP, + DEFAULT_LINEAR_TRAVEL_PER_SUBSTEP); + } + + private static float approximateMinimumExtent(@Nonnull ShapeType shapeType, + boolean hasBoxHalfExtents, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius) { + if (shapeType == ShapeType.BOX && hasBoxHalfExtents) { + return Math.max(MIN_LINEAR_TRAVEL_PER_SUBSTEP, + Math.min(halfExtentX, Math.min(halfExtentY, halfExtentZ))); + } + if (shapeType == ShapeType.SPHERE + || shapeType == ShapeType.CAPSULE + || shapeType == ShapeType.CYLINDER + || shapeType == ShapeType.CONE) { + return Math.max(MIN_LINEAR_TRAVEL_PER_SUBSTEP, radius); + } + return DEFAULT_LINEAR_TRAVEL_PER_SUBSTEP; + } + + private static float approximateShapeRadius(@Nonnull ShapeType shapeType, + boolean hasBoxHalfExtents, + float halfExtentX, + float halfExtentY, + float halfExtentZ, + float radius, + float halfHeight) { + if (shapeType == ShapeType.BOX && hasBoxHalfExtents) { + return Math.max(MIN_LINEAR_TRAVEL_PER_SUBSTEP, + (float) Math.sqrt(halfExtentX * halfExtentX + + halfExtentY * halfExtentY + + halfExtentZ * halfExtentZ)); + } + if (shapeType == ShapeType.SPHERE) { + return Math.max(MIN_LINEAR_TRAVEL_PER_SUBSTEP, radius); + } + if (shapeType == ShapeType.CAPSULE + || shapeType == ShapeType.CYLINDER + || shapeType == ShapeType.CONE) { + return Math.max(MIN_LINEAR_TRAVEL_PER_SUBSTEP, radius + halfHeight); + } + return DEFAULT_LINEAR_TRAVEL_PER_SUBSTEP; + } + + private static float vectorLength(float x, float y, float z) { + return (float) Math.sqrt(x * x + y * y + z * z); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsBodyAttachmentIndexSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsBodyAttachmentIndexSystem.java deleted file mode 100644 index 2fabfaa7..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsBodyAttachmentIndexSystem.java +++ /dev/null @@ -1,74 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.sync; - -import com.hypixel.hytale.component.CommandBuffer; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.query.Query; -import com.hypixel.hytale.component.system.RefChangeSystem; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Keeps the body-key to entity-ref attachment index in sync with ECS component changes. - */ -public class PhysicsBodyAttachmentIndexSystem - extends RefChangeSystem { - - private static final ComponentType ATTACHMENT_TYPE = - PhysicsBodyAttachmentComponent.getComponentType(); - private static final Query QUERY = ATTACHMENT_TYPE; - - @Override - public void onComponentAdded(@Nonnull Ref ref, - @Nonnull PhysicsBodyAttachmentComponent component, - @Nonnull Store store, - @Nonnull CommandBuffer commandBuffer) { - PhysicsWorldRuntimeResource.require( - commandBuffer.getResource(PhysicsWorldResource.getResourceType())) - .registerBodyAttachment(component.getBodyKey(), ref); - } - - @Override - public void onComponentSet(@Nonnull Ref ref, - @Nullable PhysicsBodyAttachmentComponent oldComponent, - @Nonnull PhysicsBodyAttachmentComponent newComponent, - @Nonnull Store store, - @Nonnull CommandBuffer commandBuffer) { - PhysicsWorldRuntimeResource resource = PhysicsWorldRuntimeResource.require( - commandBuffer.getResource(PhysicsWorldResource.getResourceType())); - assert oldComponent != null; - if (!oldComponent.getBodyKey().equals(newComponent.getBodyKey())) { - resource.unregisterBodyAttachment(oldComponent.getBodyKey(), ref); - resource.registerBodyAttachment(newComponent.getBodyKey(), ref); - } - resource.clearBodySyncState(ref); - } - - @Override - public void onComponentRemoved(@Nonnull Ref ref, - @Nonnull PhysicsBodyAttachmentComponent component, - @Nonnull Store store, - @Nonnull CommandBuffer commandBuffer) { - PhysicsWorldRuntimeResource resource = PhysicsWorldRuntimeResource.require( - commandBuffer.getResource(PhysicsWorldResource.getResourceType())); - resource.unregisterBodyAttachment(component.getBodyKey(), ref); - resource.clearBodySyncState(ref); - } - - @Nonnull - @Override - public ComponentType componentType() { - return ATTACHMENT_TYPE; - } - - @Nonnull - @Override - public Query getQuery() { - return QUERY; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsSyncSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsSyncSystem.java deleted file mode 100644 index 0d271eb6..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsSyncSystem.java +++ /dev/null @@ -1,437 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.sync; - -import com.hypixel.hytale.component.ArchetypeChunk; -import com.hypixel.hytale.component.CommandBuffer; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.dependency.Dependency; -import com.hypixel.hytale.component.dependency.Order; -import com.hypixel.hytale.component.dependency.SystemDependency; -import com.hypixel.hytale.component.dependency.SystemGroupDependency; -import com.hypixel.hytale.component.query.Query; -import com.hypixel.hytale.component.system.tick.EntityTickingSystem; -import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; -import com.hypixel.hytale.server.core.modules.entity.system.TransformSystems; -import com.hypixel.hytale.server.core.modules.entity.system.UpdateLocationSystems; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.internal.math.PhysicsVisualPoseMath; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRuntimeState; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import dev.hytalemodding.impulse.core.internal.systems.visual.GeneratedProxyLifecycle; -import dev.hytalemodding.impulse.core.internal.systems.visual.VisualInterestCollector; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent.AttachmentLifecycle; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyRegistrationView; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import java.util.List; -import java.util.Set; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Quaternionf; -import org.joml.Vector3d; -import org.joml.Vector3f; - -/** - * Synchronizes physics bodies with Hytale transforms each tick. - * - *

Runs after the persistence restore group so that newly bootstrapped spaces, - * hydrated bodies, and hydrated joints are all settled before this system reads - * body transforms.

- * - *

Entities attach to body keys. Backend body destruction is explicit at the - * world resource boundary; missing generated visual proxies are removed, while - * gameplay entities merely lose the attachment.

- */ -public class PhysicsSyncSystem extends EntityTickingSystem { - - private static final ComponentType ATTACHMENT_TYPE = - PhysicsBodyAttachmentComponent.getComponentType(); - private static final ComponentType TRANSFORM_TYPE = - TransformComponent.getComponentType(); - - private static final Query QUERY = Query.and(ATTACHMENT_TYPE, TRANSFORM_TYPE); - private final Set> dependencies = Set.of( - new SystemGroupDependency<>(Order.AFTER, ImpulsePlugin.get().getPersistenceRestoreGroup()), - new SystemDependency<>(Order.BEFORE, TransformSystems.EntityTrackerUpdate.class), - new SystemDependency<>(Order.BEFORE, UpdateLocationSystems.TickingSystem.class) - ); - - /* - * Low-speed uncontrolled dynamic bodies get a wider visual deadzone and a - * slower keepalive. Controlled bodies bypass this classification so player - * input stays responsive. - */ - private static final float LOW_SPEED_LINEAR_THRESHOLD_SQUARED = 0.2f * 0.2f; - private static final float LOW_SPEED_ANGULAR_THRESHOLD_SQUARED = 0.5f * 0.5f; - private static final float MIN_PREDICTED_ANGULAR_SPEED = 1.0e-4f; - private static final float MIN_SMOOTHING_ALPHA = 0.05f; - private static final float MAX_SMOOTHING_TELEPORT_DISTANCE_SQUARED = 4.0f * 4.0f; - - @Nonnull - private final ThreadLocal> playerInterests = - ThreadLocal.withInitial(List::of); - @Nonnull - private final ThreadLocal syncNanos = ThreadLocal.withInitial(() -> 0L); - - /** - * Hytale may run entity ticks in parallel. Each tick task needs independent temporary objects - * because the backend out-parameter getters write into caller-owned vectors. - */ - private final ThreadLocal scratch = ThreadLocal.withInitial(Scratch::new); - - @Override - public boolean isParallel(int archetypeChunkSize, int taskCount) { - // Backend bodies and per-body sync state are owned by the world tick thread. - // Body poses are read from the world-level snapshot cache before parallel ECS writes. - return false; - } - - @Override - public void tick(float dt, int systemIndex, @Nonnull Store store) { - playerInterests.set(VisualInterestCollector.collectSyncInterests(store)); - syncNanos.set(System.nanoTime()); - PhysicsRuntimeProfilingResource profiling = store.getResource( - PhysicsRuntimeProfilingResource.getResourceType()); - PhysicsRuntimeProfilingResource.SyncCollector collector = profiling.isEnabled() - ? profiling.beginSyncSample() : null; - long startNanos = collector != null ? System.nanoTime() : 0L; - try { - super.tick(dt, systemIndex, store); - } finally { - if (collector != null) { - profiling.finishSyncSample(collector, System.nanoTime() - startNanos); - } - playerInterests.remove(); - syncNanos.remove(); - } - } - - @Override - public void tick(float dt, - int index, - @Nonnull ArchetypeChunk chunk, - @Nonnull Store store, - @Nonnull CommandBuffer commandBuffer) { - Ref entityRef = chunk.getReferenceTo(index); - PhysicsBodyAttachmentComponent attachment = chunk.getComponent(index, ATTACHMENT_TYPE); - TransformComponent transform = chunk.getComponent(index, TRANSFORM_TYPE); - if (attachment == null || transform == null) { - return; - } - - Scratch local = scratch.get(); - PhysicsWorldRuntimeResource resource = local.getResource(store); - PhysicsRuntimeProfilingResource.SyncCollector collector = local.getSyncCollector(store); - if (collector != null) { - collector.incrementBodiesInspected(); - } - PhysicsBodyRegistrationView registration = - resource.getBodyRegistrationView(attachment.getBodyKey()); - if (registration == null) { - if (resource.isBodyCreationPending(attachment.getBodyKey())) { - return; - } - GeneratedProxyLifecycle.clearMissingAttachment(entityRef, attachment, resource, commandBuffer); - return; - } - - SpaceId spaceId = registration.spaceId(); - if (resource.getSpaceBinding(spaceId) == null) { - if (collector != null) { - collector.incrementSkippedMissingSpace(); - } - GeneratedProxyLifecycle.clearMissingAttachment(entityRef, attachment, resource, commandBuffer); - return; - } - - if (!PhysicsTransformAuthority.shouldApplyBodyTransform(attachment)) { - return; - } - - PhysicsBodySnapshot snapshot = resource.getBodySnapshotIfRegistered(registration.bodyKey()); - if (snapshot == null) { - GeneratedProxyLifecycle.clearMissingAttachment(entityRef, attachment, resource, commandBuffer); - return; - } - if (snapshot.isStatic()) { - if (collector != null) { - collector.incrementSkippedStatic(); - } - return; - } - - PhysicsSpaceSettings settings = resolveSpaceSettings(resource, spaceId); - snapshot.copyPositionTo(local.position); - snapshot.copyRotationTo(local.rotation); - applyVisualPose(snapshot, attachment, local); - PhysicsBodyRuntimeState.BodySyncState syncState = resource.getOrCreateBodySyncState(entityRef); - if (collector != null) { - collector.recordBodySnapshotMotion(syncState.recordSnapshotObservation(local.visualPosition)); - } else { - syncState.recordSnapshotObservation(local.visualPosition); - } - - boolean sleeping = snapshot.sleeping(); - boolean controlled = resource.isBodyControlled(registration.bodyKey()); - boolean lowSpeed = false; - if (!sleeping && snapshot.isDynamic() && !controlled) { - snapshot.copyLinearVelocityTo(local.linearVelocity); - snapshot.copyAngularVelocityTo(local.angularVelocity); - lowSpeed = local.linearVelocity.lengthSquared() <= LOW_SPEED_LINEAR_THRESHOLD_SQUARED - && local.angularVelocity.lengthSquared() <= LOW_SPEED_ANGULAR_THRESHOLD_SQUARED; - } - - boolean rangeLimitedVisual = shouldCullVisualSync(settings, attachment, controlled); - PhysicsSyncPolicy.SyncRangeTier rangeTier = PhysicsSyncPolicy.resolveRangeTier( - settings, - resource.getBodyVisualInterestState(registration.bodyKey()), - rangeLimitedVisual, - controlled, - playerInterests.get(), - local.visualPosition); - if (rangeTier == PhysicsSyncPolicy.SyncRangeTier.NEAR) { - snapshot.copyPositionTo(local.position); - snapshot.copyRotationTo(local.rotation); - applySnapshotPrediction(snapshot, - PhysicsSyncPolicy.visualPredictionSeconds(settings, - syncNanos.get(), - resource.getLatestSnapshotAppliedNanos()), - local); - applyVisualPose(snapshot, attachment, local); - } - - PhysicsSyncPolicy.SyncDecision decision = PhysicsSyncPolicy.resolveSyncDecision(syncState, - settings, - local.visualPosition, - local.visualRotation, - sleeping, - lowSpeed, - controlled, - rangeTier); - if (decision == PhysicsSyncPolicy.SyncDecision.SKIP_SLEEPING - || decision == PhysicsSyncPolicy.SyncDecision.SKIP_THRESHOLD - || decision == PhysicsSyncPolicy.SyncDecision.SKIP_VISUAL_DEADZONE - || decision == PhysicsSyncPolicy.SyncDecision.SKIP_VISUAL_RANGE) { - syncState.recordSkip(dt); - if (collector != null) { - if (decision == PhysicsSyncPolicy.SyncDecision.SKIP_SLEEPING) { - collector.incrementSkippedSleeping(); - } else if (decision == PhysicsSyncPolicy.SyncDecision.SKIP_VISUAL_DEADZONE) { - collector.incrementSkippedVisualDeadzone(); - } else if (decision == PhysicsSyncPolicy.SyncDecision.SKIP_VISUAL_RANGE) { - collector.incrementSkippedVisualRange(); - } else { - collector.incrementSkippedThreshold(); - } - } - return; - } - - if (shouldSmoothVisual(settings, snapshot, controlled, rangeTier, syncState, decision)) { - applyVisualSmoothing(settings, dt, syncState, local); - } - - if (collector != null) { - collector.recordVisualCorrection(distance(transform.getPosition(), local.visualPosition)); - } - transform.getPosition().set(local.visualPosition.x, - local.visualPosition.y, - local.visualPosition.z); - local.visualRotation.getEulerAnglesYXZ(local.euler); - transform.getRotation().set(local.euler.x, local.euler.y, local.euler.z); - syncState.recordSync(local.visualPosition, local.visualRotation, sleeping); - if (collector != null) { - collector.incrementBodiesSynced(); - if (decision == PhysicsSyncPolicy.SyncDecision.TRANSITION) { - collector.incrementTransitionSyncs(); - } else if (decision == PhysicsSyncPolicy.SyncDecision.KEEPALIVE) { - collector.incrementKeepaliveSyncs(); - } - } - } - - private static float distance(@Nonnull Vector3d from, @Nonnull Vector3f to) { - double dx = from.x - to.x; - double dy = from.y - to.y; - double dz = from.z - to.z; - double distanceSquared = dx * dx + dy * dy + dz * dz; - if (!Double.isFinite(distanceSquared)) { - return Float.NaN; - } - return (float) Math.sqrt(distanceSquared); - } - - private void applyVisualPose(@Nonnull PhysicsBodySnapshot snapshot, - @Nonnull PhysicsBodyAttachmentComponent attachment, - @Nonnull Scratch scratch) { - PhysicsVisualPoseMath.visualPositionFromBodyPose(scratch.position, - scratch.rotation, - attachment.resolveVisualOriginOffsetY(snapshot.centerOfMassOffsetY()), - attachment.getLocalPositionOffset(), - scratch.visualPosition, - scratch.worldOffset); - scratch.visualRotation.set(scratch.rotation); - - scratch.visualRotation.mul(attachment.getLocalRotationOffset()); - } - - private static boolean shouldSmoothVisual(@Nullable PhysicsSpaceSettings settings, - @Nonnull PhysicsBodySnapshot snapshot, - boolean controlled, - @Nonnull PhysicsSyncPolicy.SyncRangeTier rangeTier, - @Nonnull PhysicsBodyRuntimeState.BodySyncState syncState, - @Nonnull PhysicsSyncPolicy.SyncDecision decision) { - return settings != null - && settings.getVisualSyncSettings().isVisualSnapshotSmoothingEnabled() - && !controlled - && rangeTier == PhysicsSyncPolicy.SyncRangeTier.NEAR - && snapshot.isDynamic() - && !snapshot.sleeping() - && syncState.isInitialized() - && decision != PhysicsSyncPolicy.SyncDecision.INITIAL - && decision != PhysicsSyncPolicy.SyncDecision.TRANSITION; - } - - private static void applyVisualSmoothing(@Nonnull PhysicsSpaceSettings settings, - float dt, - @Nonnull PhysicsBodyRuntimeState.BodySyncState syncState, - @Nonnull Scratch scratch) { - if (scratch.visualPosition.distanceSquared(syncState.getLastSyncedPosition()) - > MAX_SMOOTHING_TELEPORT_DISTANCE_SQUARED) { - return; - } - float alpha = smoothingAlpha(settings, dt); - scratch.smoothingTargetPosition.set(scratch.visualPosition); - scratch.visualPosition.set(syncState.getLastSyncedPosition()) - .lerp(scratch.smoothingTargetPosition, alpha); - - scratch.smoothingTargetRotation.set(scratch.visualRotation); - scratch.visualRotation.set(syncState.getLastSyncedRotation()) - .slerp(scratch.smoothingTargetRotation, alpha) - .normalize(); - } - - static float smoothingAlpha(@Nonnull PhysicsSpaceSettings settings, float dt) { - if (!Float.isFinite(dt) || dt <= 0.0f) { - return 1.0f; - } - return Math.clamp(dt * settings.getVisualSyncSettings().getVisualSnapshotSmoothingRate(), - MIN_SMOOTHING_ALPHA, 1.0f); - } - - private static void applySnapshotPrediction(@Nonnull PhysicsBodySnapshot snapshot, - float predictionSeconds, - @Nonnull Scratch scratch) { - if (predictionSeconds <= 0.0f || !snapshot.isDynamic() || snapshot.sleeping()) { - return; - } - - snapshot.copyLinearVelocityTo(scratch.linearVelocity); - if (isFinite(scratch.linearVelocity)) { - scratch.position.fma(predictionSeconds, scratch.linearVelocity); - } - - snapshot.copyAngularVelocityTo(scratch.angularVelocity); - if (!isFinite(scratch.angularVelocity)) { - return; - } - float angularSpeed = scratch.angularVelocity.length(); - if (angularSpeed <= MIN_PREDICTED_ANGULAR_SPEED) { - return; - } - float inverseAngularSpeed = 1.0f / angularSpeed; - scratch.predictedRotation.rotationAxis(angularSpeed * predictionSeconds, - scratch.angularVelocity.x * inverseAngularSpeed, - scratch.angularVelocity.y * inverseAngularSpeed, - scratch.angularVelocity.z * inverseAngularSpeed); - scratch.rotation.mul(scratch.predictedRotation).normalize(); - } - - private static boolean isFinite(@Nonnull Vector3f vector) { - return Float.isFinite(vector.x) - && Float.isFinite(vector.y) - && Float.isFinite(vector.z); - } - - @Nullable - private static PhysicsSpaceSettings resolveSpaceSettings(@Nonnull PhysicsWorldRuntimeResource resource, - @Nullable SpaceId spaceId) { - if (spaceId != null) { - return resource.getLiveSpaceSettings(spaceId); - } - return null; - } - - private static boolean shouldCullVisualSync(@Nullable PhysicsSpaceSettings settings, - @Nonnull PhysicsBodyAttachmentComponent attachment, - boolean controlled) { - if (controlled) { - return false; - } - if (attachment.getLifecycle() == AttachmentLifecycle.GENERATED_PROXY) { - return true; - } - return settings != null && settings.getVisualSyncSettings().isEntityVisualSyncCullingEnabled(); - } - - private static final class Scratch { - - private final Vector3f position = new Vector3f(); - private final Quaternionf rotation = new Quaternionf(); - private final Vector3f visualPosition = new Vector3f(); - private final Quaternionf visualRotation = new Quaternionf(); - private final Vector3f worldOffset = new Vector3f(); - private final Vector3f euler = new Vector3f(); - private final Vector3f linearVelocity = new Vector3f(); - private final Vector3f angularVelocity = new Vector3f(); - private final Quaternionf predictedRotation = new Quaternionf(); - private final Vector3f smoothingTargetPosition = new Vector3f(); - private final Quaternionf smoothingTargetRotation = new Quaternionf(); - - @Nullable - private Store cachedResourceStore; - @Nullable - private Store cachedProfilingStore; - @Nullable - private PhysicsWorldRuntimeResource cachedResource; - @Nullable - private PhysicsRuntimeProfilingResource cachedProfiling; - - @Nonnull - private PhysicsWorldRuntimeResource getResource(@Nonnull Store store) { - if (cachedResourceStore != store || cachedResource == null) { - cachedResourceStore = store; - cachedResource = PhysicsWorldRuntimeResource.require(store); - } - return cachedResource; - } - - @Nullable - private PhysicsRuntimeProfilingResource.SyncCollector getSyncCollector( - @Nonnull Store store) { - if (cachedProfilingStore != store || cachedProfiling == null) { - cachedProfilingStore = store; - cachedProfiling = store.getResource(PhysicsRuntimeProfilingResource.getResourceType()); - } - return cachedProfiling.isEnabled() ? cachedProfiling.getActiveSyncCollector() : null; - } - } - - @Nonnull - @Override - public Query getQuery() { - return QUERY; - } - - @Nonnull - @Override - public Set> getDependencies() { - return dependencies; - } - -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsTransformAuthority.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsTransformAuthority.java deleted file mode 100644 index 470bfe71..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsTransformAuthority.java +++ /dev/null @@ -1,15 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.sync; - -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent.TransformAuthority; -import javax.annotation.Nonnull; - -final class PhysicsTransformAuthority { - - private PhysicsTransformAuthority() { - } - - static boolean shouldApplyBodyTransform(@Nonnull PhysicsBodyAttachmentComponent attachment) { - return attachment.getTransformAuthority() == TransformAuthority.BODY; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/DetachedVisualGeometry.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/DetachedVisualGeometry.java deleted file mode 100644 index 2b03171e..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/DetachedVisualGeometry.java +++ /dev/null @@ -1,62 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.visual; - -import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime.VisualInterest; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import java.util.List; -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -final class DetachedVisualGeometry { - - /* - * Approximate visibility policy for optional visual culling. Close bodies - * bypass the cone so players do not lose nearby proxies while turning. - */ - private static final float VIEW_CONE_DOT = 0.35f; - private static final float VIEW_CONE_NEAR_RADIUS_SQUARED = 8.0f * 8.0f; - - private DetachedVisualGeometry() { - } - - static float visibleDistanceSquared(float positionX, - float positionY, - float positionZ, - @Nonnull PhysicsSpaceSettings settings, - @Nonnull List interests, - float radius) { - float radiusSquared = radius * radius; - float nearestDistanceSquared = Float.POSITIVE_INFINITY; - for (VisualInterest interest : interests) { - float dx = positionX - interest.position().x; - float dy = positionY - interest.position().y; - float dz = positionZ - interest.position().z; - float distanceSquared = dx * dx + dy * dy + dz * dz; - if (distanceSquared <= radiusSquared - && isInsideViewCone(settings, interest, dx, dy, dz, distanceSquared)) { - nearestDistanceSquared = Math.min(nearestDistanceSquared, distanceSquared); - } - } - return nearestDistanceSquared; - } - - static boolean isInsideViewCone(@Nonnull PhysicsSpaceSettings settings, - @Nonnull VisualInterest interest, - float dx, - float dy, - float dz, - float distanceSquared) { - if (!settings.getVisualSyncSettings().isVisualVisibilityCullingEnabled() - || interest.direction() == null - || distanceSquared <= VIEW_CONE_NEAR_RADIUS_SQUARED) { - return true; - } - - float length = (float) Math.sqrt(distanceSquared); - if (length <= 0.0f) { - return true; - } - Vector3f direction = interest.direction(); - float dot = (dx * direction.x + dy * direction.y + dz * direction.z) / length; - return dot >= VIEW_CONE_DOT; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/DetachedVisualOcclusion.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/DetachedVisualOcclusion.java deleted file mode 100644 index ffb7116a..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/DetachedVisualOcclusion.java +++ /dev/null @@ -1,204 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.visual; - -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime.VisualInterest; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.VisualOcclusionMode; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RaycastClosestQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.view.RaycastHitView; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3f; - -final class DetachedVisualOcclusion { - - private static final ThreadLocal RAYCAST_TARGET = - ThreadLocal.withInitial(Vector3f::new); - - private DetachedVisualOcclusion() { - } - - @Nonnull - static Result resolve(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull RigidBodyKey bodyKey, - @Nullable PhysicsSpaceBinding space, - @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull PhysicsSpaceSettings settings, - @Nonnull List interests, - float radius, - long visualInterestTick, - @Nonnull RaycastBudget raycastBudget, - @Nullable PhysicsRuntimeProfilingResource.VisualCollector collector) { - InterestProbe probe = probeNearestLikelyInterest(snapshot, settings, interests, radius); - PhysicsVisualRuntime.BodyVisualInterestState state = - resource.getOrCreateBodyVisualInterestState(bodyKey); - if (!probe.inRange()) { - state.clearPendingRaycast(); - state.recordInterest(Float.POSITIVE_INFINITY, false, false, false, visualInterestTick); - return Result.notVisible(); - } - - VisualOcclusionMode occlusionMode = settings.getVisualSyncSettings().getVisualOcclusionMode(); - if (occlusionMode == VisualOcclusionMode.OFF || space == null) { - state.clearPendingRaycast(); - state.recordInterest(probe.distanceSquared(), true, true, false, visualInterestTick); - return Result.visible(probe.distanceSquared(), probe.distanceSquared()); - } - - boolean raycastFresh = state.hasFreshRaycast(settings.getVisualSyncSettings() - .getVisualOcclusionCacheTicks(), - visualInterestTick); - boolean raycastDecisionKnown = raycastFresh; - boolean raycastVisible = raycastFresh && state.isRaycastVisible(); - boolean raycastEvaluated = false; - if (raycastFresh && collector != null) { - collector.incrementRaycastCacheHits(); - } - - if (state.hasCompletedRaycast()) { - Optional completedRaycast = state.pollCompletedRaycast(); - raycastVisible = completedRaycast - .map(view -> bodyKey.equals(view.bodyKey())) - .orElse(false); - raycastDecisionKnown = true; - raycastEvaluated = true; - } else if (!raycastFresh) { - if (state.hasRaycastResult()) { - raycastVisible = state.isRaycastVisible(); - raycastDecisionKnown = true; - } - if (!state.hasPendingRaycast() && raycastBudget.tryUse(settings)) { - submitRaycast(resource, space, state, probe, snapshot); - if (collector != null) { - collector.incrementRaycasts(); - } - } - } - - state.recordInterest(probe.distanceSquared(), - true, - raycastVisible, - raycastEvaluated, - visualInterestTick); - if (occlusionMode == VisualOcclusionMode.CULL && raycastDecisionKnown && !raycastVisible) { - return Result.notVisible(); - } - - /* - * PRIORITY only biases spawn order: visible bodies move forward in the - * queue and occluded bodies move back. CULL above is the mode that skips - * occluded candidates entirely. - */ - float priorityDistanceSquared = probe.distanceSquared(); - if (occlusionMode == VisualOcclusionMode.PRIORITY && raycastDecisionKnown) { - priorityDistanceSquared = raycastVisible - ? probe.distanceSquared() * 0.25f - : probe.distanceSquared() + radius * radius; - } - return Result.visible(probe.distanceSquared(), priorityDistanceSquared); - } - - private static void submitRaycast(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull PhysicsSpaceBinding space, - @Nonnull PhysicsVisualRuntime.BodyVisualInterestState state, - @Nonnull InterestProbe probe, - @Nonnull PhysicsBodySnapshot snapshot) { - VisualInterest interest = Objects.requireNonNull(probe.interest(), "interest"); - Vector3f target = RAYCAST_TARGET.get() - .set(snapshot.positionX(), snapshot.positionY(), snapshot.positionZ()); - state.startPendingRaycast(resource.query(new RaycastClosestQuery(space.spaceId(), - interest.position(), - target)).completion()); - } - - @Nonnull - private static InterestProbe probeNearestLikelyInterest(@Nonnull PhysicsBodySnapshot snapshot, - @Nonnull PhysicsSpaceSettings settings, - @Nonnull List interests, - float radius) { - return probeNearestLikelyInterest(snapshot.positionX(), - snapshot.positionY(), - snapshot.positionZ(), - settings, - interests, - radius); - } - - @Nonnull - private static InterestProbe probeNearestLikelyInterest(float positionX, - float positionY, - float positionZ, - @Nonnull PhysicsSpaceSettings settings, - @Nonnull List interests, - float radius) { - float radiusSquared = radius * radius; - float nearestDistanceSquared = Float.POSITIVE_INFINITY; - VisualInterest nearestInterest = null; - for (VisualInterest interest : interests) { - float dx = positionX - interest.position().x; - float dy = positionY - interest.position().y; - float dz = positionZ - interest.position().z; - float distanceSquared = dx * dx + dy * dy + dz * dz; - if (distanceSquared <= radiusSquared - && distanceSquared < nearestDistanceSquared - && DetachedVisualGeometry.isInsideViewCone(settings, - interest, - dx, - dy, - dz, - distanceSquared)) { - nearestDistanceSquared = distanceSquared; - nearestInterest = interest; - } - } - return nearestInterest == null - ? InterestProbe.notVisible() - : new InterestProbe(nearestInterest, nearestDistanceSquared); - } - - record Result(boolean shouldMaterialize, - float distanceSquared, - float priorityDistanceSquared) { - - static Result notVisible() { - return new Result(false, Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY); - } - - static Result visible(float distanceSquared, float priorityDistanceSquared) { - return new Result(true, distanceSquared, priorityDistanceSquared); - } - } - - static final class RaycastBudget { - - private int used; - - boolean tryUse(@Nonnull PhysicsSpaceSettings settings) { - if (used >= settings.getVisualSyncSettings().getVisualOcclusionRaycastsPerTick()) { - return false; - } - used++; - return true; - } - } - - private record InterestProbe(@Nullable VisualInterest interest, - float distanceSquared) { - - static InterestProbe notVisible() { - return new InterestProbe(null, Float.POSITIVE_INFINITY); - } - - boolean inRange() { - return interest != null; - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/GameplayAttachmentSnapshot.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/GameplayAttachmentSnapshot.java deleted file mode 100644 index d9216dea..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/GameplayAttachmentSnapshot.java +++ /dev/null @@ -1,82 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.visual; - -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent.AttachmentLifecycle; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.ConcurrentLinkedQueue; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -final class GameplayAttachmentSnapshot { - - @Nonnull - private final BodyKeySource source; - @Nullable - private Set bodyKeys; - - private GameplayAttachmentSnapshot(@Nonnull BodyKeySource source) { - this.source = source; - } - - @Nonnull - static GameplayAttachmentSnapshot forStore(@Nonnull Store store) { - return fromSource(() -> collectGameplayAttachmentBodyKeys(store)); - } - - @Nonnull - static GameplayAttachmentSnapshot fromSource(@Nonnull BodyKeySource source) { - return new GameplayAttachmentSnapshot(source); - } - - boolean hasKnownGameplayAttachment(boolean runtimeIndexHasAttachment, - @Nonnull RigidBodyKey bodyKey) { - if (runtimeIndexHasAttachment) { - return true; - } - return hasGameplayAttachment(bodyKey); - } - - boolean hasGameplayAttachment(@Nonnull RigidBodyKey bodyKey) { - return bodyKeys().contains(bodyKey); - } - - @Nonnull - private Set bodyKeys() { - if (bodyKeys == null) { - bodyKeys = source.bodyKeys(); - } - return bodyKeys; - } - - @Nonnull - private static Set collectGameplayAttachmentBodyKeys( - @Nonnull Store store) { - ComponentType attachmentType = - PhysicsBodyAttachmentComponent.getComponentType(); - Queue bodyKeys = new ConcurrentLinkedQueue<>(); - store.forEachEntityParallel(attachmentType, - (index, archetypeChunk, _) -> { - PhysicsBodyAttachmentComponent attachment = archetypeChunk.getComponent(index, - attachmentType); - if (attachment != null && attachment.getLifecycle() != AttachmentLifecycle.GENERATED_PROXY) { - bodyKeys.add(attachment.getBodyKey()); - } - }); - Set uniqueBodyKeys = new ObjectOpenHashSet<>(); - uniqueBodyKeys.addAll(bodyKeys); - return uniqueBodyKeys; - } - - @FunctionalInterface - interface BodyKeySource { - - @Nonnull - Set bodyKeys(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/PhysicsDetachedVisualMaterializationSystem.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/PhysicsDetachedVisualMaterializationSystem.java deleted file mode 100644 index 24585ca6..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/systems/visual/PhysicsDetachedVisualMaterializationSystem.java +++ /dev/null @@ -1,814 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.visual; - -import com.hypixel.hytale.component.AddReason; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Holder; -import com.hypixel.hytale.component.NonSerialized; -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.component.dependency.Dependency; -import com.hypixel.hytale.component.dependency.Order; -import com.hypixel.hytale.component.dependency.SystemDependency; -import com.hypixel.hytale.component.dependency.SystemGroupDependency; -import com.hypixel.hytale.component.system.tick.TickingSystem; -import com.hypixel.hytale.math.util.ChunkUtil; -import com.hypixel.hytale.server.core.entity.entities.BlockEntity; -import com.hypixel.hytale.server.core.modules.entity.DespawnComponent; -import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; -import com.hypixel.hytale.server.core.modules.physics.component.Velocity; -import com.hypixel.hytale.server.core.modules.time.TimeResource; -import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk; -import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.internal.components.GeneratedVisualProxyComponent; -import dev.hytalemodding.impulse.core.internal.math.PhysicsVisualPoseMath; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime.VisualInterest; -import dev.hytalemodding.impulse.core.internal.systems.sync.PhysicsSyncSystem; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyRegistrationView; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent.AttachmentLifecycle; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent.TransformAuthority; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsVisualMaterializationSettings; -import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Queue; -import java.util.Set; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.function.ToIntFunction; -import java.util.WeakHashMap; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Quaternionf; -import org.joml.Vector3d; -import org.joml.Vector3f; - -/** - * Materializes disposable Hytale visual followers for detached physics bodies near players. - * - *

Detached bodies stay physics-authoritative and are not persisted through these visual - * proxies. Proxies are ordinary Hytale block entities with a generated - * {@link PhysicsBodyAttachmentComponent}, so removing a proxy never removes the backend body.

- */ -public class PhysicsDetachedVisualMaterializationSystem extends TickingSystem { - - private final Set> dependencies = Set.of( - new SystemGroupDependency<>(Order.AFTER, ImpulsePlugin.get().getPersistenceRestoreGroup()), - new SystemDependency<>(Order.BEFORE, PhysicsSyncSystem.class) - ); - - /** - * Bounds generated-proxy orphan scans. Regular materialized-proxy - * visibility checks are cached separately below. - */ - private static final int ORPHAN_VISUAL_CLEANUP_INTERVAL_TICKS = 40; - @Nonnull - private final Map, MaterializationState> statesByStore = - Collections.synchronizedMap(new WeakHashMap<>()); - - @Override - public void tick(float dt, int systemIndex, @Nonnull Store store) { - PhysicsRuntimeProfilingResource profiling = store.getResource( - PhysicsRuntimeProfilingResource.getResourceType()); - PhysicsRuntimeProfilingResource.VisualCollector collector = profiling.isEnabled() - ? profiling.beginVisualSample() - : null; - long tickStart = collector != null ? System.nanoTime() : 0L; - try { - tickMaterialization(store, collector); - } finally { - if (collector != null) { - profiling.finishVisualSample(collector, System.nanoTime() - tickStart); - } - } - } - - private void tickMaterialization(@Nonnull Store store, - @Nullable PhysicsRuntimeProfilingResource.VisualCollector collector) { - MaterializationState state = stateFor(store); - PersistentPhysicsWorldResource persistent = store.getResource( - PersistentPhysicsWorldResource.getResourceType()); - if (shouldPauseForRestore(state, persistent)) { - return; - } - PhysicsWorldRuntimeResource resource = PhysicsWorldRuntimeResource.require(store); - if (state.orphanVisualCleanupCooldown <= 0) { - removeOrphanVisualFollowers(store, resource); - state.orphanVisualCleanupCooldown = ORPHAN_VISUAL_CLEANUP_INTERVAL_TICKS; - } else { - state.orphanVisualCleanupCooldown--; - } - long visualInterestTick = resource.advanceVisualInterestTick(); - GameplayAttachmentSnapshot gameplayAttachments = GameplayAttachmentSnapshot.forStore(store); - List interests = currentVisualInterests(state, - store, - resource, - collector); - DetachedVisualOcclusion.RaycastBudget raycastBudget = - new DetachedVisualOcclusion.RaycastBudget(); - int materialized = currentMaterializedProxyCount(state, - store, - resource, - interests, - gameplayAttachments, - collector); - refreshCachedMaterializationTargets(state, - store, - resource, - interests, - visualInterestTick, - raycastBudget, - gameplayAttachments, - collector); - materialized += spawnCachedMaterializationTargets(state, - store, - resource, - materialized, - interests, - visualInterestTick, - raycastBudget, - gameplayAttachments, - collector); - if (collector != null) { - collector.setMaterialized(materialized); - } - } - - private static boolean shouldPauseForRestore(@Nonnull MaterializationState state, - @Nonnull PersistentPhysicsWorldResource persistent) { - long restoreGeneration = persistent.runtimeRestoreGeneration(); - if (state.observedRestoreGeneration != restoreGeneration) { - state.observedRestoreGeneration = restoreGeneration; - state.cachedMaterializationTargets.clear(); - state.materializationCandidateRefreshCooldown = 0; - state.materializedVisibilityCheckCooldown = 0; - return true; - } - if (!isRestoreBlockingVisuals(persistent)) { - return false; - } - state.cachedMaterializationTargets.clear(); - state.materializationCandidateRefreshCooldown = 0; - state.materializedVisibilityCheckCooldown = 0; - return true; - } - - static boolean isRestoreBlockingVisuals(@Nonnull PersistentPhysicsWorldResource persistent) { - return persistent.isRuntimeRestorePending() || persistent.hasRuntimeRestoreFailed(); - } - - @Nonnull - private MaterializationState stateFor(@Nonnull Store store) { - synchronized (statesByStore) { - return statesByStore.computeIfAbsent(store, ignored -> new MaterializationState()); - } - } - - @Nonnull - private List currentVisualInterests( - @Nonnull MaterializationState state, - @Nonnull Store store, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nullable PhysicsRuntimeProfilingResource.VisualCollector collector) { - int refreshInterval = resolveVisualInterestRefreshInterval(resource); - state.visualInterestRefreshCooldown = Math.min(state.visualInterestRefreshCooldown, - refreshCooldown(refreshInterval)); - if (state.visualInterestRefreshCooldown <= 0) { - state.cachedInterests = VisualInterestCollector.collectMaterializationInterests(store, resource); - state.visualInterestRefreshCooldown = refreshCooldown(refreshInterval); - state.materializationCandidateRefreshCooldown = 0; - } else { - state.visualInterestRefreshCooldown--; - } - if (collector != null) { - collector.setInterests(state.cachedInterests.size()); - } - return state.cachedInterests; - } - - private int currentMaterializedProxyCount(@Nonnull MaterializationState state, - @Nonnull Store store, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull List interests, - @Nonnull GameplayAttachmentSnapshot gameplayAttachments, - @Nullable PhysicsRuntimeProfilingResource.VisualCollector collector) { - int refreshInterval = resolveMaterializedVisibilityCheckInterval(resource); - state.materializedVisibilityCheckCooldown = Math.min(state.materializedVisibilityCheckCooldown, - refreshCooldown(refreshInterval)); - if (state.materializedVisibilityCheckCooldown <= 0) { - state.materializedVisibilityCheckCooldown = refreshCooldown(refreshInterval); - return processMaterializedProxies(store, resource, interests, gameplayAttachments, collector); - } - - state.materializedVisibilityCheckCooldown--; - int materialized = resource.getGeneratedVisualProxyCount(); - if (collector != null) { - collector.addVisibilityCheckSkips(materialized); - } - return materialized; - } - - private void refreshCachedMaterializationTargets(@Nonnull MaterializationState state, - @Nonnull Store store, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull List interests, - long visualInterestTick, - @Nonnull DetachedVisualOcclusion.RaycastBudget raycastBudget, - @Nonnull GameplayAttachmentSnapshot gameplayAttachments, - @Nullable PhysicsRuntimeProfilingResource.VisualCollector collector) { - if (interests.isEmpty()) { - state.cachedMaterializationTargets.clear(); - state.materializationCandidateRefreshCooldown = 0; - if (collector != null) { - collector.setCandidates(0); - } - return; - } - - int refreshInterval = resolveMaterializationCandidateRefreshInterval(resource); - state.materializationCandidateRefreshCooldown = Math.min( - state.materializationCandidateRefreshCooldown, - refreshCooldown(refreshInterval)); - if (state.materializationCandidateRefreshCooldown <= 0) { - List refreshedTargets = new ArrayList<>(); - if (collector != null) { - collector.incrementCandidateRefreshes(); - } - collectMaterializationCandidates(store, - resource, - interests, - visualInterestTick, - raycastBudget, - gameplayAttachments, - refreshedTargets, - collector); - refreshedTargets.removeIf(Objects::isNull); - refreshedTargets.sort(Comparator.comparingDouble( - CachedMaterializationTarget::distanceSquared)); - state.cachedMaterializationTargets.clear(); - state.cachedMaterializationTargets.addAll(refreshedTargets); - state.materializationCandidateRefreshCooldown = refreshCooldown(refreshInterval); - } else { - state.materializationCandidateRefreshCooldown--; - if (collector != null) { - collector.incrementCandidateCacheUses(); - } - } - - if (collector != null) { - collector.setCandidates(state.cachedMaterializationTargets.size()); - } - } - - private int spawnCachedMaterializationTargets(@Nonnull MaterializationState state, - @Nonnull Store store, - @Nonnull PhysicsWorldRuntimeResource resource, - int materialized, - @Nonnull List interests, - long visualInterestTick, - @Nonnull DetachedVisualOcclusion.RaycastBudget raycastBudget, - @Nonnull GameplayAttachmentSnapshot gameplayAttachments, - @Nullable PhysicsRuntimeProfilingResource.VisualCollector collector) { - int spawned = 0; - int index = 0; - while (index < state.cachedMaterializationTargets.size()) { - CachedMaterializationTarget target = state.cachedMaterializationTargets.get(index); - if (target == null) { - state.cachedMaterializationTargets.remove(index); - continue; - } - MaterializationCandidate candidate = resolveCachedMaterializationCandidate(store, - resource, - target, - interests, - visualInterestTick, - raycastBudget, - gameplayAttachments, - collector); - if (candidate == null) { - state.cachedMaterializationTargets.remove(index); - continue; - } - if (spawned >= candidate.settings().getVisualMaterializationSettings().getDetachedVisualMaxSpawnsPerTick() - || materialized >= candidate.settings().getVisualMaterializationSettings().getDetachedVisualMaxMaterialized()) { - index++; - continue; - } - state.cachedMaterializationTargets.remove(index); - Ref proxy = spawnProxy(store, - candidate.bodyKey(), - candidate.snapshot(), - candidate.registration(), - candidate.settings()); - if (proxy == null) { - continue; - } - resource.setGeneratedVisualProxy(candidate.bodyKey(), proxy); - spawned++; - materialized++; - if (collector != null) { - collector.incrementSpawned(); - } - } - return spawned; - } - - @Nullable - private static MaterializationCandidate resolveCachedMaterializationCandidate( - @Nonnull Store store, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull CachedMaterializationTarget target, - @Nonnull List interests, - long visualInterestTick, - @Nonnull DetachedVisualOcclusion.RaycastBudget raycastBudget, - @Nonnull GameplayAttachmentSnapshot gameplayAttachments, - @Nullable PhysicsRuntimeProfilingResource.VisualCollector collector) { - if (resource.getGeneratedVisualProxy(target.bodyKey()) != null) { - return null; - } - - PhysicsBodyRegistrationView registration = resource.getBodyRegistrationView(target.bodyKey()); - if (registration == null - || registration.kind() != PhysicsBodyKind.BODY - || !sameSpaceId(registration.spaceId(), target.spaceId()) - || gameplayAttachments.hasKnownGameplayAttachment( - resource.hasBodyAttachments(registration.bodyKey()), - registration.bodyKey())) { - return null; - } - - PhysicsSpaceSettings settings = resolveSettings(resource, registration); - if (settings == null || !settings.getVisualMaterializationSettings().isDetachedVisualMaterializationEnabled()) { - return null; - } - PhysicsSpaceBinding space = resolveSpace(resource, registration); - if (space == null) { - return null; - } - - PhysicsBodySnapshot snapshot = resource.getBodySnapshotIfRegistered(target.bodyKey()); - if (snapshot == null) { - return null; - } - if (!isBodyChunkLoaded(store, snapshot)) { - return null; - } - DetachedVisualOcclusion.Result currentPolicy = - resolveCurrentMaterializationPolicy(resource, - target.bodyKey(), - space, - snapshot, - settings, - interests, - visualInterestTick, - raycastBudget, - collector); - if (!currentPolicy.shouldMaterialize()) { - return null; - } - return new MaterializationCandidate(target.bodyKey(), - snapshot, - registration, - settings, - currentPolicy.priorityDistanceSquared()); - } - - private static int refreshCooldown(int intervalTicks) { - return Math.max(0, intervalTicks - 1); - } - - private static int resolveVisualInterestRefreshInterval( - @Nonnull PhysicsWorldRuntimeResource resource) { - return resolveMinimumDetachedVisualInterval(resource, - settings -> settings.getVisualMaterializationSettings() - .getDetachedVisualInterestRefreshIntervalTicks(), - PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_INTEREST_REFRESH_INTERVAL_TICKS); - } - - private static int resolveMaterializationCandidateRefreshInterval( - @Nonnull PhysicsWorldRuntimeResource resource) { - return resolveMinimumDetachedVisualInterval(resource, - settings -> settings.getVisualMaterializationSettings() - .getDetachedVisualCandidateRefreshIntervalTicks(), - PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_CANDIDATE_REFRESH_INTERVAL_TICKS); - } - - private static int resolveMaterializedVisibilityCheckInterval( - @Nonnull PhysicsWorldRuntimeResource resource) { - return resolveMinimumDetachedVisualInterval(resource, - settings -> settings.getVisualMaterializationSettings() - .getDetachedVisualVisibilityCheckIntervalTicks(), - PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_VISIBILITY_CHECK_INTERVAL_TICKS); - } - - private static int resolveMinimumDetachedVisualInterval( - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull ToIntFunction intervalGetter, - int defaultInterval) { - int interval = Integer.MAX_VALUE; - for (PhysicsSpaceBinding space : resource.iterateSpaceBindings()) { - PhysicsSpaceSettings settings = resource.getLiveSpaceSettings(space.spaceId()); - if (settings.getVisualMaterializationSettings().isDetachedVisualMaterializationEnabled()) { - interval = Math.min(interval, intervalGetter.applyAsInt(settings)); - } - } - return interval == Integer.MAX_VALUE ? defaultInterval : interval; - } - - private static int processMaterializedProxies(@Nonnull Store store, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull List interests, - @Nonnull GameplayAttachmentSnapshot gameplayAttachments, - @Nullable PhysicsRuntimeProfilingResource.VisualCollector collector) { - int count = 0; - for (RigidBodyKey bodyKey : resource.getGeneratedVisualProxyBodyKeys()) { - if (collector != null) { - collector.incrementVisibilityChecks(); - } - PhysicsBodyRegistrationView registration = resource.getBodyRegistrationView(bodyKey); - if (registration == null || registration.kind() != PhysicsBodyKind.BODY) { - if (registration == null && resource.isBodyCreationPending(bodyKey)) { - count++; - continue; - } - GeneratedProxyLifecycle.removeProxy(store, resource, bodyKey); - if (collector != null) { - collector.incrementDematerialized(); - } - continue; - } - Ref proxy = resource.getGeneratedVisualProxy(bodyKey); - if (proxy == null || !isExpectedProxy(store, proxy, bodyKey, registration.spaceId())) { - GeneratedProxyLifecycle.removeProxy(store, resource, bodyKey); - if (collector != null) { - collector.incrementDematerialized(); - } - continue; - } - if (hasGameplayAttachment(store, resource, bodyKey, proxy, gameplayAttachments)) { - GeneratedProxyLifecycle.removeProxy(store, resource, bodyKey); - if (collector != null) { - collector.incrementDematerialized(); - } - continue; - } - - PhysicsSpaceSettings settings = resolveSettings(resource, registration); - if (settings == null || !settings.getVisualMaterializationSettings().isDetachedVisualMaterializationEnabled()) { - GeneratedProxyLifecycle.removeProxy(store, resource, bodyKey); - if (collector != null) { - collector.incrementDematerialized(); - } - continue; - } - - PhysicsBodySnapshot snapshot = resource.getBodySnapshotIfRegistered(bodyKey); - if (snapshot == null) { - GeneratedProxyLifecycle.removeProxy(store, resource, bodyKey); - if (collector != null) { - collector.incrementDematerialized(); - } - continue; - } - if (!isBodyChunkLoaded(store, snapshot) - || shouldDematerialize(snapshot, settings, interests)) { - GeneratedProxyLifecycle.removeProxy(store, resource, bodyKey); - if (collector != null) { - collector.incrementDematerialized(); - } - continue; - } - count++; - } - return count; - } - - private static void collectMaterializationCandidates(@Nonnull Store store, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull List interests, - long visualInterestTick, - @Nonnull DetachedVisualOcclusion.RaycastBudget raycastBudget, - @Nonnull GameplayAttachmentSnapshot gameplayAttachments, - @Nonnull List candidates, - @Nullable PhysicsRuntimeProfilingResource.VisualCollector collector) { - if (interests.isEmpty()) { - return; - } - - Set seenBodies = new ObjectOpenHashSet<>(); - for (PhysicsSpaceBinding space : resource.iterateSpaceBindings()) { - PhysicsSpaceSettings settings = resource.getLiveSpaceSettings(space.spaceId()); - if (!settings.getVisualMaterializationSettings().isDetachedVisualMaterializationEnabled()) { - continue; - } - - for (int interestIndex = 0; interestIndex < interests.size(); interestIndex++) { - VisualInterest interest = interests.get(interestIndex); - if (collector != null) { - collector.incrementNearQueries(); - } - int nearCandidates = resource.forEachIndexedBodySnapshotNear(space.spaceId(), - interest.position(), - settings.getVisualMaterializationSettings().getDetachedVisualMaterializationRadius(), - (bodyKey, snapshot, bodySpaceId, kind, persistenceMode) -> { - if (!seenBodies.add(bodyKey) || resource.getGeneratedVisualProxy(bodyKey) != null) { - return; - } - if (kind != PhysicsBodyKind.BODY - || !bodySpaceId.equals(space.spaceId()) - || gameplayAttachments.hasKnownGameplayAttachment( - resource.hasBodyAttachments(bodyKey), - bodyKey)) { - return; - } - if (!isBodyChunkLoaded(store, snapshot)) { - return; - } - DetachedVisualOcclusion.Result materializeInterest = - DetachedVisualOcclusion.resolve(resource, - bodyKey, - space, - snapshot, - settings, - interests, - settings.getVisualMaterializationSettings().getDetachedVisualMaterializationRadius(), - visualInterestTick, - raycastBudget, - collector); - if (materializeInterest.shouldMaterialize()) { - candidates.add(new CachedMaterializationTarget(bodyKey, - bodySpaceId, - materializeInterest.priorityDistanceSquared())); - } - }); - if (collector != null) { - collector.addNearQueryCandidates(nearCandidates); - } - } - } - } - - private static void removeOrphanVisualFollowers(@Nonnull Store store, - @Nonnull PhysicsWorldRuntimeResource resource) { - Queue orphanProxies = new ConcurrentLinkedQueue<>(); - ComponentType attachmentType = - PhysicsBodyAttachmentComponent.getComponentType(); - store.forEachEntityParallel(attachmentType, - (index, archetypeChunk, commandBuffer) -> { - PhysicsBodyAttachmentComponent attachment = archetypeChunk.getComponent(index, - attachmentType); - if (attachment == null - || attachment.getLifecycle() != AttachmentLifecycle.GENERATED_PROXY) { - return; - } - - var ref = archetypeChunk.getReferenceTo(index); - orphanProxies.add(new OrphanVisualProxy(attachment.getBodyKey(), - attachment.getSpaceId(), - ref)); - }); - - for (OrphanVisualProxy proxy : orphanProxies) { - if (!hasLiveVisualTarget(resource, proxy.bodyKey(), proxy.spaceId(), proxy.ref())) { - GeneratedProxyLifecycle.removeProxy(store, resource, proxy.bodyKey(), proxy.ref()); - } - } - } - - private static boolean hasLiveVisualTarget(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull RigidBodyKey bodyKey, - @Nullable SpaceId spaceId, - @Nonnull Ref proxyRef) { - PhysicsBodyRegistrationView registration = resource.getBodyRegistrationView(bodyKey); - if (registration == null) { - return resource.isBodyCreationPending(bodyKey) - && resource.isGeneratedVisualProxy(bodyKey, proxyRef); - } - if (!sameSpaceId(registration.spaceId(), spaceId)) { - return false; - } - return resource.getSpaceBinding(registration.spaceId()) != null - && resource.isGeneratedVisualProxy(bodyKey, proxyRef); - } - - private record OrphanVisualProxy( - @Nonnull RigidBodyKey bodyKey, - @Nullable SpaceId spaceId, - @Nonnull Ref ref - ) { - } - - private static boolean hasGameplayAttachment(@Nonnull Store store, - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull RigidBodyKey bodyKey, - @Nonnull Ref proxy, - @Nonnull GameplayAttachmentSnapshot gameplayAttachments) { - ComponentType attachmentType = - PhysicsBodyAttachmentComponent.getComponentType(); - for (Ref attachmentRef : resource.getBodyAttachments(bodyKey)) { - if (attachmentRef == proxy || attachmentRef.equals(proxy)) { - continue; - } - PhysicsBodyAttachmentComponent attachment = store.getComponent(attachmentRef, - attachmentType); - if (attachment != null && attachment.getLifecycle() != AttachmentLifecycle.GENERATED_PROXY) { - return true; - } - } - return gameplayAttachments.hasGameplayAttachment(bodyKey); - } - - @Nullable - private static PhysicsSpaceSettings resolveSettings(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull PhysicsBodyRegistrationView registration) { - if (resource.getSpaceBinding(registration.spaceId()) != null) { - return resource.getLiveSpaceSettings(registration.spaceId()); - } - return null; - } - - @Nullable - private static PhysicsSpaceBinding resolveSpace(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull PhysicsBodyRegistrationView registration) { - return resource.getSpaceBinding(registration.spaceId()); - } - - private static boolean shouldMaterialize(@Nonnull PhysicsBodySnapshot snapshot, - @Nonnull PhysicsSpaceSettings settings, - @Nonnull List interests) { - if (interests.isEmpty()) { - return false; - } - return visibleDistanceSquared(snapshot, - settings, - interests, - settings.getVisualMaterializationSettings().getDetachedVisualMaterializationRadius()) != Float.POSITIVE_INFINITY; - } - - @Nonnull - static DetachedVisualOcclusion.Result resolveCurrentMaterializationPolicy( - @Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsSpaceBinding space, - @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull PhysicsSpaceSettings settings, - @Nonnull List interests, - long visualInterestTick, - @Nonnull DetachedVisualOcclusion.RaycastBudget raycastBudget, - @Nullable PhysicsRuntimeProfilingResource.VisualCollector collector) { - return DetachedVisualOcclusion.resolve(resource, - bodyKey, - space, - snapshot, - settings, - interests, - settings.getVisualMaterializationSettings().getDetachedVisualMaterializationRadius(), - visualInterestTick, - raycastBudget, - collector); - } - - private static boolean shouldDematerialize(@Nonnull PhysicsBodySnapshot snapshot, - @Nonnull PhysicsSpaceSettings settings, - @Nonnull List interests) { - if (interests.isEmpty()) { - return true; - } - return visibleDistanceSquared(snapshot, - settings, - interests, - settings.getVisualMaterializationSettings().getDetachedVisualDematerializationRadius()) == Float.POSITIVE_INFINITY; - } - - private static float visibleDistanceSquared(@Nonnull PhysicsBodySnapshot snapshot, - @Nonnull PhysicsSpaceSettings settings, - @Nonnull List interests, - float radius) { - return DetachedVisualGeometry.visibleDistanceSquared(snapshot.positionX(), - snapshot.positionY(), - snapshot.positionZ(), - settings, - interests, - radius); - } - - private static boolean isBodyChunkLoaded(@Nonnull Store store, - @Nonnull PhysicsBodySnapshot snapshot) { - ChunkStore chunkStore = store.getExternalData().getWorld().getChunkStore(); - Store chunkComponentStore = chunkStore.getStore(); - Ref chunkRef = chunkStore.getChunkReference(ChunkUtil.indexChunk( - ChunkUtil.chunkCoordinate(snapshot.positionX()), - ChunkUtil.chunkCoordinate(snapshot.positionZ()))); - if (chunkRef == null || !chunkRef.isValid()) { - return false; - } - - WorldChunk worldChunk = chunkComponentStore.getComponentConcurrent(chunkRef, - WorldChunk.getComponentType()); - return worldChunk != null; - } - - private static boolean isExpectedProxy(@Nonnull Store store, - @Nonnull Ref proxy, - @Nonnull RigidBodyKey bodyKey, - @Nullable SpaceId spaceId) { - if (!proxy.isValid()) { - return false; - } - PhysicsBodyAttachmentComponent attachment = - store.getComponent(proxy, PhysicsBodyAttachmentComponent.getComponentType()); - return attachment != null - && attachment.getLifecycle() == AttachmentLifecycle.GENERATED_PROXY - && attachment.getBodyKey().equals(bodyKey) - && sameSpaceId(attachment.getSpaceId(), spaceId); - } - - private static boolean sameSpaceId(@Nullable SpaceId first, @Nullable SpaceId second) { - return Objects.equals(first, second); - } - - @Nullable - private static Ref spawnProxy(@Nonnull Store store, - @Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull PhysicsBodyRegistrationView registration, - @Nonnull PhysicsSpaceSettings settings) { - TimeResource time = store.getResource(TimeResource.getResourceType()); - Quaternionf rotation = new Quaternionf(snapshot.rotationX(), - snapshot.rotationY(), - snapshot.rotationZ(), - snapshot.rotationW()); - Vector3f visualPosition = PhysicsVisualPoseMath.visualPositionFromBodyPose(new Vector3f(snapshot.positionX(), - snapshot.positionY(), - snapshot.positionZ()), - rotation, - snapshot.centerOfMassOffsetY(), - new Vector3f(), - new Vector3f()); - Holder holder = BlockEntity.assembleDefaultBlockEntity( - time, - settings.getVisualMaterializationSettings().getDetachedVisualBlockType(), - new Vector3d(visualPosition.x, visualPosition.y, visualPosition.z)); - TransformComponent transform = holder.getComponent(TransformComponent.getComponentType()); - if (transform != null) { - Vector3f euler = rotation.getEulerAnglesYXZ(new Vector3f()); - transform.getRotation().set(euler.x, euler.y, euler.z); - } - holder.removeComponent(DespawnComponent.getComponentType()); - holder.removeComponent(Velocity.getComponentType()); - holder.addComponent(store.getRegistry().getNonSerializedComponentType(), NonSerialized.get()); - holder.addComponent(GeneratedVisualProxyComponent.getComponentType(), new GeneratedVisualProxyComponent()); - holder.addComponent(PhysicsBodyAttachmentComponent.getComponentType(), - new PhysicsBodyAttachmentComponent(bodyKey, - registration.spaceId(), - TransformAuthority.BODY, - AttachmentLifecycle.GENERATED_PROXY)); - return store.addEntity(holder, AddReason.SPAWN); - } - - private record MaterializationCandidate(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull PhysicsBodyRegistrationView registration, - @Nonnull PhysicsSpaceSettings settings, - float distanceSquared) { - } - - private record CachedMaterializationTarget(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - float distanceSquared) { - } - - private static final class MaterializationState { - - private int orphanVisualCleanupCooldown; - private int visualInterestRefreshCooldown; - private int materializationCandidateRefreshCooldown; - private int materializedVisibilityCheckCooldown; - private long observedRestoreGeneration; - @Nonnull - private List cachedInterests = List.of(); - @Nonnull - private final List cachedMaterializationTargets = - new ArrayList<>(); - } - - @Nonnull - @Override - public Set> getDependencies() { - return dependencies; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/body/PhysicsBodyKind.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/body/PhysicsBodyKind.java deleted file mode 100644 index 0a154fa8..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/body/PhysicsBodyKind.java +++ /dev/null @@ -1,11 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.body; - - -/** - * Classifies why a body exists in the runtime registry. - */ -public enum PhysicsBodyKind { - BODY, - WORLD_COLLISION, - TEMPORARY -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/body/PhysicsBodyPersistenceMode.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/body/PhysicsBodyPersistenceMode.java deleted file mode 100644 index 07c9eea8..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/body/PhysicsBodyPersistenceMode.java +++ /dev/null @@ -1,10 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.body; - - -/** - * Controls whether a registered body participates in Impulse persistence. - */ -public enum PhysicsBodyPersistenceMode { - PERSISTENT, - RUNTIME_ONLY -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/body/PhysicsBodyRegistrationView.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/body/PhysicsBodyRegistrationView.java deleted file mode 100644 index 8b661d9e..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/body/PhysicsBodyRegistrationView.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.body; - -import dev.hytalemodding.impulse.api.SpaceId; -import javax.annotation.Nonnull; - -/** - * Immutable body registration metadata safe for public and off-owner callers. - */ -public record PhysicsBodyRegistrationView(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/body/RigidBodyKey.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/body/RigidBodyKey.java deleted file mode 100644 index 7db6ed12..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/body/RigidBodyKey.java +++ /dev/null @@ -1,83 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.body; - -import dev.hytalemodding.impulse.core.internal.math.UuidMath; -import java.util.Objects; -import java.util.concurrent.ThreadLocalRandom; -import java.util.UUID; -import javax.annotation.Nonnull; - -/** - * Stable Impulse-side identity for a physics body. - * - *

Backend {@code PhysicsBody} handles may change when spaces migrate between - * backends. This id is the durable handle used by ECS attachments, persistence, - * snapshots, and physics-owner command queues.

- */ -public final class RigidBodyKey { - - private final long mostSignificantBits; - private final long leastSignificantBits; - - public RigidBodyKey(@Nonnull UUID value) { - this(Objects.requireNonNull(value, "value").getMostSignificantBits(), - value.getLeastSignificantBits()); - } - - private RigidBodyKey(long mostSignificantBits, - long leastSignificantBits) { - this.mostSignificantBits = mostSignificantBits; - this.leastSignificantBits = leastSignificantBits; - } - - @Nonnull - public static RigidBodyKey random() { - ThreadLocalRandom random = ThreadLocalRandom.current(); - long mostSignificantBits = UuidMath.version4MostSignificantBits(random.nextLong()); - long leastSignificantBits = UuidMath.ietfVariantLeastSignificantBits(random.nextLong()); - return new RigidBodyKey(mostSignificantBits, leastSignificantBits); - } - - @Nonnull - public static RigidBodyKey of(@Nonnull UUID value) { - return new RigidBodyKey(value); - } - - @Nonnull - public static RigidBodyKey of(long mostSignificantBits, - long leastSignificantBits) { - return new RigidBodyKey(mostSignificantBits, leastSignificantBits); - } - - @Nonnull - public UUID value() { - return new UUID(mostSignificantBits, leastSignificantBits); - } - - public long mostSignificantBits() { - return mostSignificantBits; - } - - public long leastSignificantBits() { - return leastSignificantBits; - } - - @Override - public boolean equals(Object other) { - return this == other - || other instanceof RigidBodyKey key - && mostSignificantBits == key.mostSignificantBits - && leastSignificantBits == key.leastSignificantBits; - } - - @Override - public int hashCode() { - long bits = mostSignificantBits ^ leastSignificantBits; - return (int) (bits >> Integer.SIZE) ^ (int) bits; - } - - @Nonnull - @Override - public String toString() { - return value().toString(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/BodyCommandComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/BodyCommandComponent.java new file mode 100644 index 00000000..ecdd96ef --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/BodyCommandComponent.java @@ -0,0 +1,458 @@ +package dev.hytalemodding.impulse.core.plugin.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.codec.codecs.EnumCodec; +import com.hypixel.hytale.codec.codecs.array.ArrayCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; +import java.util.Arrays; +import java.util.Objects; +import javax.annotation.Nonnull; +import lombok.Getter; +import org.joml.Vector3f; + +/** + * One-tick ordered body commands drained by PhysicsStore systems. + */ +public final class BodyCommandComponent implements Component { + + private static final Entry[] EMPTY_ENTRIES = new Entry[0]; + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + BodyCommandComponent.class, + BodyCommandComponent::new) + .append(new KeyedCodec<>("Commands", + new ArrayCodec<>(Entry.CODEC, Entry[]::new), + false), + (component, value) -> component.entries = copyEntries(value), + BodyCommandComponent::entries) + .add() + .build(); + + @Nonnull + private Entry[] entries = EMPTY_ENTRIES; + + public BodyCommandComponent() { + } + + private BodyCommandComponent(@Nonnull Entry[] entries) { + this.entries = copyEntries(entries); + } + + @Nonnull + public static BodyCommandComponent wake() { + return new BodyCommandComponent(new Entry[] {Entry.wake()}); + } + + @Nonnull + public static BodyCommandComponent sleep() { + return new BodyCommandComponent(new Entry[] {Entry.sleep()}); + } + + @Nonnull + public static BodyCommandComponent setType(@Nonnull PhysicsBodyType bodyType, + boolean activate) { + return new BodyCommandComponent(new Entry[] {Entry.setType(bodyType, activate)}); + } + + @Nonnull + public static BodyCommandComponent setCollisionFilter(int collisionGroup, + int collisionMask, + boolean activate) { + return new BodyCommandComponent(new Entry[] { + Entry.setCollisionFilter(collisionGroup, collisionMask, activate) + }); + } + + @Nonnull + public static BodyCommandComponent setVelocity(@Nonnull Vector3f linearVelocity, + @Nonnull Vector3f angularVelocity, + boolean activate) { + return new BodyCommandComponent(new Entry[] { + Entry.setVelocity(linearVelocity, angularVelocity, activate) + }); + } + + @Nonnull + public static BodyCommandComponent vector(@Nonnull Kind kind, + float x, + float y, + float z, + boolean hasOffset, + float offsetX, + float offsetY, + float offsetZ) { + return new BodyCommandComponent(new Entry[] { + Entry.vector(kind, x, y, z, hasOffset, offsetX, offsetY, offsetZ) + }); + } + + @Nonnull + public BodyCommandComponent append(@Nonnull BodyCommandComponent other) { + Entry[] otherEntries = other.entries(); + if (otherEntries.length == 0) { + return clone(); + } + Entry[] merged = Arrays.copyOf(entries, entries.length + otherEntries.length); + for (int index = 0; index < otherEntries.length; index++) { + merged[entries.length + index] = otherEntries[index].clone(); + } + return new BodyCommandComponent(merged); + } + + @Nonnull + public Entry[] entries() { + return copyEntries(entries); + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.bodyCommandComponentType(); + } + + @Nonnull + @Override + public BodyCommandComponent clone() { + return new BodyCommandComponent(entries); + } + + @Nonnull + private static Entry[] copyEntries(Entry[] entries) { + if (entries == null || entries.length == 0) { + return EMPTY_ENTRIES; + } + return Arrays.stream(entries) + .filter(Objects::nonNull) + .map(Entry::clone) + .toArray(Entry[]::new); + } + + /** + * One ordered body command entry. + */ + public static final class Entry { + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder(Entry.class, + Entry::new) + .append(new KeyedCodec<>("Kind", new EnumCodec<>(Kind.class), false), + (entry, value) -> entry.kind = value != null ? value : Kind.WAKE, + Entry::getKind) + .add() + .append(new KeyedCodec<>("BodyType", new EnumCodec<>(PhysicsBodyType.class), false), + (entry, value) -> entry.bodyType = value != null + ? value + : PhysicsBodyType.DYNAMIC, + Entry::getBodyType) + .add() + .append(new KeyedCodec<>("Activate", Codec.BOOLEAN, false), + (entry, value) -> entry.activate = value != null && value, + Entry::isActivate) + .add() + .append(new KeyedCodec<>("X", Codec.FLOAT, false), + (entry, value) -> entry.x = value != null ? value : 0.0f, + Entry::getX) + .add() + .append(new KeyedCodec<>("Y", Codec.FLOAT, false), + (entry, value) -> entry.y = value != null ? value : 0.0f, + Entry::getY) + .add() + .append(new KeyedCodec<>("Z", Codec.FLOAT, false), + (entry, value) -> entry.z = value != null ? value : 0.0f, + Entry::getZ) + .add() + .append(new KeyedCodec<>("HasOffset", Codec.BOOLEAN, false), + (entry, value) -> entry.hasOffset = value != null && value, + Entry::hasOffset) + .add() + .append(new KeyedCodec<>("OffsetX", Codec.FLOAT, false), + (entry, value) -> entry.offsetX = value != null ? value : 0.0f, + Entry::getOffsetX) + .add() + .append(new KeyedCodec<>("OffsetY", Codec.FLOAT, false), + (entry, value) -> entry.offsetY = value != null ? value : 0.0f, + Entry::getOffsetY) + .add() + .append(new KeyedCodec<>("OffsetZ", Codec.FLOAT, false), + (entry, value) -> entry.offsetZ = value != null ? value : 0.0f, + Entry::getOffsetZ) + .add() + .append(new KeyedCodec<>("AngularX", Codec.FLOAT, false), + (entry, value) -> entry.angularX = value != null ? value : 0.0f, + Entry::getAngularX) + .add() + .append(new KeyedCodec<>("AngularY", Codec.FLOAT, false), + (entry, value) -> entry.angularY = value != null ? value : 0.0f, + Entry::getAngularY) + .add() + .append(new KeyedCodec<>("AngularZ", Codec.FLOAT, false), + (entry, value) -> entry.angularZ = value != null ? value : 0.0f, + Entry::getAngularZ) + .add() + .append(new KeyedCodec<>("CollisionGroup", Codec.INTEGER, false), + (entry, value) -> entry.collisionGroup = value != null + ? value + : PhysicsCollisionFilters.DYNAMIC_BODY, + Entry::getCollisionGroup) + .add() + .append(new KeyedCodec<>("CollisionMask", Codec.INTEGER, false), + (entry, value) -> entry.collisionMask = value != null + ? value + : PhysicsCollisionFilters.ALL, + Entry::getCollisionMask) + .add() + .build(); + + @Nonnull + private Kind kind = Kind.WAKE; + @Nonnull + private PhysicsBodyType bodyType = PhysicsBodyType.DYNAMIC; + @Getter + private boolean activate; + @Getter + private float x; + @Getter + private float y; + @Getter + private float z; + private boolean hasOffset; + @Getter + private float offsetX; + @Getter + private float offsetY; + @Getter + private float offsetZ; + @Getter + private float angularX; + @Getter + private float angularY; + @Getter + private float angularZ; + @Getter + private int collisionGroup = PhysicsCollisionFilters.DYNAMIC_BODY; + @Getter + private int collisionMask = PhysicsCollisionFilters.ALL; + + public Entry() { + } + + private Entry(@Nonnull Kind kind, + @Nonnull PhysicsBodyType bodyType, + boolean activate, + float x, + float y, + float z, + boolean hasOffset, + float offsetX, + float offsetY, + float offsetZ, + float angularX, + float angularY, + float angularZ, + int collisionGroup, + int collisionMask) { + this.kind = Objects.requireNonNull(kind, "kind"); + this.bodyType = Objects.requireNonNull(bodyType, "bodyType"); + this.activate = activate; + this.x = x; + this.y = y; + this.z = z; + this.hasOffset = hasOffset; + this.offsetX = offsetX; + this.offsetY = offsetY; + this.offsetZ = offsetZ; + this.angularX = angularX; + this.angularY = angularY; + this.angularZ = angularZ; + this.collisionGroup = collisionGroup; + this.collisionMask = collisionMask; + } + + @Nonnull + private static Entry wake() { + return new Entry(Kind.WAKE, + PhysicsBodyType.DYNAMIC, + true, + 0.0f, + 0.0f, + 0.0f, + false, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + PhysicsCollisionFilters.DYNAMIC_BODY, + PhysicsCollisionFilters.ALL); + } + + @Nonnull + private static Entry sleep() { + return new Entry(Kind.SLEEP, + PhysicsBodyType.DYNAMIC, + false, + 0.0f, + 0.0f, + 0.0f, + false, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + PhysicsCollisionFilters.DYNAMIC_BODY, + PhysicsCollisionFilters.ALL); + } + + @Nonnull + private static Entry setType(@Nonnull PhysicsBodyType bodyType, + boolean activate) { + return new Entry(Kind.SET_TYPE, + bodyType, + activate, + 0.0f, + 0.0f, + 0.0f, + false, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + PhysicsCollisionFilters.DYNAMIC_BODY, + PhysicsCollisionFilters.ALL); + } + + @Nonnull + private static Entry setCollisionFilter(int collisionGroup, + int collisionMask, + boolean activate) { + return new Entry(Kind.SET_COLLISION_FILTER, + PhysicsBodyType.DYNAMIC, + activate, + 0.0f, + 0.0f, + 0.0f, + false, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + collisionGroup, + collisionMask); + } + + @Nonnull + private static Entry setVelocity(@Nonnull Vector3f linearVelocity, + @Nonnull Vector3f angularVelocity, + boolean activate) { + Objects.requireNonNull(linearVelocity, "linearVelocity"); + Objects.requireNonNull(angularVelocity, "angularVelocity"); + return new Entry(Kind.SET_VELOCITY, + PhysicsBodyType.DYNAMIC, + activate, + linearVelocity.x, + linearVelocity.y, + linearVelocity.z, + false, + 0.0f, + 0.0f, + 0.0f, + angularVelocity.x, + angularVelocity.y, + angularVelocity.z, + PhysicsCollisionFilters.DYNAMIC_BODY, + PhysicsCollisionFilters.ALL); + } + + @Nonnull + private static Entry vector(@Nonnull Kind kind, + float x, + float y, + float z, + boolean hasOffset, + float offsetX, + float offsetY, + float offsetZ) { + if (!kind.isVector()) { + throw new IllegalArgumentException("Body command kind is not vector-valued: " + kind); + } + return new Entry(kind, + PhysicsBodyType.DYNAMIC, + true, + x, + y, + z, + hasOffset, + offsetX, + offsetY, + offsetZ, + 0.0f, + 0.0f, + 0.0f, + PhysicsCollisionFilters.DYNAMIC_BODY, + PhysicsCollisionFilters.ALL); + } + + @Nonnull + public Kind getKind() { + return kind; + } + + @Nonnull + public PhysicsBodyType getBodyType() { + return bodyType; + } + + public boolean hasOffset() { + return hasOffset; + } + + @Nonnull + @Override + public Entry clone() { + return new Entry(kind, + bodyType, + activate, + x, + y, + z, + hasOffset, + offsetX, + offsetY, + offsetZ, + angularX, + angularY, + angularZ, + collisionGroup, + collisionMask); + } + } + + public enum Kind { + WAKE, + SLEEP, + IMPULSE, + TORQUE_IMPULSE, + FORCE, + TORQUE, + SET_TYPE, + SET_VELOCITY, + SET_COLLISION_FILTER; + + public boolean isVector() { + return this == IMPULSE || this == TORQUE_IMPULSE || this == FORCE || this == TORQUE; + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/BodyComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/BodyComponent.java new file mode 100644 index 00000000..cf28be94 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/BodyComponent.java @@ -0,0 +1,73 @@ +package dev.hytalemodding.impulse.core.plugin.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Authored body space ownership. + */ +public final class BodyComponent implements Component { + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + BodyComponent.class, + BodyComponent::new) + .append(new KeyedCodec<>("SpaceUuid", Codec.UUID_BINARY, false), + (component, value) -> component.spaceUuid = value, + BodyComponent::getSpaceUuid) + .add() + .build(); + + @Nonnull + private UUID spaceUuid = new UUID(0L, 0L); + @Nullable + private transient Ref spaceRef; + + public BodyComponent() { + } + + public BodyComponent(@Nonnull UUID spaceUuid) { + this.spaceUuid = Objects.requireNonNull(spaceUuid, "spaceUuid"); + } + + @Nonnull + public UUID getSpaceUuid() { + return spaceUuid; + } + + public void setSpaceUuid(@Nonnull UUID spaceUuid) { + this.spaceUuid = Objects.requireNonNull(spaceUuid, "spaceUuid"); + this.spaceRef = null; + } + + @Nullable + public Ref getSpaceRef() { + return spaceRef; + } + + public void setSpaceRef(@Nullable Ref spaceRef) { + this.spaceRef = spaceRef; + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.bodyComponentType(); + } + + @Nonnull + @Override + public BodyComponent clone() { + BodyComponent copy = new BodyComponent(spaceUuid); + copy.spaceRef = spaceRef; + return copy; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/ColliderComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/ColliderComponent.java new file mode 100644 index 00000000..beffc7b9 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/ColliderComponent.java @@ -0,0 +1,97 @@ +package dev.hytalemodding.impulse.core.plugin.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.math.vector.Vector3fUtil; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.codec.ImpulseCodecs; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +/** + * Local collider settings for one body entity. + */ +public final class ColliderComponent implements Component { + + private static final Vector3f ZERO = new Vector3f(); + private static final Quaternionf IDENTITY = new Quaternionf(); + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + ColliderComponent.class, + ColliderComponent::new) + .append(new KeyedCodec<>("LocalPosition", Vector3fUtil.CODEC, false), + (component, value) -> component.localPosition.set(value != null ? value : ZERO), + ColliderComponent::getLocalPosition) + .add() + .append(new KeyedCodec<>("LocalRotation", ImpulseCodecs.QUATERNIONF, false), + (component, value) -> component.localRotation.set(value != null ? value : IDENTITY), + ColliderComponent::getLocalRotation) + .add() + .append(new KeyedCodec<>("Sensor", Codec.BOOLEAN, false), + (component, value) -> component.sensor = value != null && value, + ColliderComponent::isSensor) + .add() + .build(); + + @Nonnull + private final Vector3f localPosition = new Vector3f(); + @Nonnull + private final Quaternionf localRotation = new Quaternionf(); + private boolean sensor; + + public ColliderComponent() { + } + + public ColliderComponent(@Nonnull Vector3f localPosition, + @Nonnull Quaternionf localRotation, + boolean sensor) { + this.localPosition.set(Objects.requireNonNull(localPosition, "localPosition")); + this.localRotation.set(Objects.requireNonNull(localRotation, "localRotation")); + this.sensor = sensor; + } + + @Nonnull + public Vector3f getLocalPosition() { + return new Vector3f(localPosition); + } + + public void setLocalPosition(@Nonnull Vector3f localPosition) { + this.localPosition.set(Objects.requireNonNull(localPosition, "localPosition")); + } + + @Nonnull + public Quaternionf getLocalRotation() { + return new Quaternionf(localRotation); + } + + public void setLocalRotation(@Nonnull Quaternionf localRotation) { + this.localRotation.set(Objects.requireNonNull(localRotation, "localRotation")); + } + + public boolean isSensor() { + return sensor; + } + + public void setSensor(boolean sensor) { + this.sensor = sensor; + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.colliderComponentType(); + } + + @Nonnull + @Override + public ColliderComponent clone() { + return new ColliderComponent(localPosition, + localRotation, + sensor); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/CollisionFilterComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/CollisionFilterComponent.java new file mode 100644 index 00000000..75482477 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/CollisionFilterComponent.java @@ -0,0 +1,72 @@ +package dev.hytalemodding.impulse.core.plugin.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; +import javax.annotation.Nonnull; + +/** + * Collision group/mask settings for one collider entity. + */ +public final class CollisionFilterComponent implements Component { + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + CollisionFilterComponent.class, + CollisionFilterComponent::new) + .append(new KeyedCodec<>("CollisionGroup", Codec.INTEGER, false), + (component, value) -> component.collisionGroup = value != null + ? value + : PhysicsCollisionFilters.DYNAMIC_BODY, + CollisionFilterComponent::getCollisionGroup) + .add() + .append(new KeyedCodec<>("CollisionMask", Codec.INTEGER, false), + (component, value) -> component.collisionMask = value != null + ? value + : PhysicsCollisionFilters.ALL, + CollisionFilterComponent::getCollisionMask) + .add() + .build(); + + private int collisionGroup = PhysicsCollisionFilters.DYNAMIC_BODY; + private int collisionMask = PhysicsCollisionFilters.ALL; + + public CollisionFilterComponent() { + } + + public CollisionFilterComponent(int collisionGroup, int collisionMask) { + this.collisionGroup = collisionGroup; + this.collisionMask = collisionMask; + } + + public int getCollisionGroup() { + return collisionGroup; + } + + public void setCollisionGroup(int collisionGroup) { + this.collisionGroup = collisionGroup; + } + + public int getCollisionMask() { + return collisionMask; + } + + public void setCollisionMask(int collisionMask) { + this.collisionMask = collisionMask; + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.collisionFilterComponentType(); + } + + @Nonnull + @Override + public CollisionFilterComponent clone() { + return new CollisionFilterComponent(collisionGroup, collisionMask); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/DynamicsComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/DynamicsComponent.java new file mode 100644 index 00000000..01423dca --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/DynamicsComponent.java @@ -0,0 +1,122 @@ +package dev.hytalemodding.impulse.core.plugin.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.codec.codecs.EnumCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * Authored motion mode, mass, damping, and CCD flags for a body entity. + */ +public final class DynamicsComponent implements Component { + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + DynamicsComponent.class, + DynamicsComponent::new) + .append(new KeyedCodec<>("BodyType", new EnumCodec<>(PhysicsBodyType.class), false), + (component, value) -> component.bodyType = value != null ? value : PhysicsBodyType.DYNAMIC, + DynamicsComponent::getBodyType) + .add() + .append(new KeyedCodec<>("Mass", Codec.FLOAT, false), + (component, value) -> component.mass = value != null ? value : 1.0f, + DynamicsComponent::getMass) + .add() + .append(new KeyedCodec<>("LinearDamping", Codec.FLOAT, false), + (component, value) -> component.linearDamping = value != null ? value : 0.0f, + DynamicsComponent::getLinearDamping) + .add() + .append(new KeyedCodec<>("AngularDamping", Codec.FLOAT, false), + (component, value) -> component.angularDamping = value != null ? value : 0.0f, + DynamicsComponent::getAngularDamping) + .add() + .append(new KeyedCodec<>("ContinuousCollision", Codec.BOOLEAN, false), + (component, value) -> component.continuousCollisionEnabled = value != null && value, + DynamicsComponent::isContinuousCollisionEnabled) + .add() + .build(); + + @Nonnull + private PhysicsBodyType bodyType = PhysicsBodyType.DYNAMIC; + private float mass = 1.0f; + private float linearDamping; + private float angularDamping; + private boolean continuousCollisionEnabled; + + public DynamicsComponent() { + } + + public DynamicsComponent(@Nonnull PhysicsBodyType bodyType, + float mass, + float linearDamping, + float angularDamping, + boolean continuousCollisionEnabled) { + this.bodyType = Objects.requireNonNull(bodyType, "bodyType"); + this.mass = mass; + this.linearDamping = linearDamping; + this.angularDamping = angularDamping; + this.continuousCollisionEnabled = continuousCollisionEnabled; + } + + @Nonnull + public PhysicsBodyType getBodyType() { + return bodyType; + } + + public void setBodyType(@Nonnull PhysicsBodyType bodyType) { + this.bodyType = Objects.requireNonNull(bodyType, "bodyType"); + } + + public float getMass() { + return mass; + } + + public void setMass(float mass) { + this.mass = mass; + } + + public float getLinearDamping() { + return linearDamping; + } + + public void setLinearDamping(float linearDamping) { + this.linearDamping = linearDamping; + } + + public float getAngularDamping() { + return angularDamping; + } + + public void setAngularDamping(float angularDamping) { + this.angularDamping = angularDamping; + } + + public boolean isContinuousCollisionEnabled() { + return continuousCollisionEnabled; + } + + public void setContinuousCollisionEnabled(boolean continuousCollisionEnabled) { + this.continuousCollisionEnabled = continuousCollisionEnabled; + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.dynamicsComponentType(); + } + + @Nonnull + @Override + public DynamicsComponent clone() { + return new DynamicsComponent(bodyType, + mass, + linearDamping, + angularDamping, + continuousCollisionEnabled); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/ExtensionSettingsComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/ExtensionSettingsComponent.java new file mode 100644 index 00000000..9a50e63e --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/ExtensionSettingsComponent.java @@ -0,0 +1,188 @@ +package dev.hytalemodding.impulse.core.plugin.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.codec.codecs.EnumCodec; +import com.hypixel.hytale.codec.codecs.array.ArrayCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsBackendExtensionId; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsExtensionSettingValue; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsExtensionSettings; +import java.util.Arrays; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * Authored backend extension settings for one PhysicsStore space entity. + */ +public final class ExtensionSettingsComponent implements Component { + + private static final Entry[] EMPTY_ENTRIES = new Entry[0]; + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + ExtensionSettingsComponent.class, + ExtensionSettingsComponent::new) + .append(new KeyedCodec<>("Settings", + new ArrayCodec<>(Entry.CODEC, Entry[]::new), + false), + (component, value) -> component.entries = copyEntries(value), + ExtensionSettingsComponent::entries) + .add() + .build(); + + @Nonnull + private Entry[] entries = EMPTY_ENTRIES; + + public ExtensionSettingsComponent() { + } + + public ExtensionSettingsComponent(@Nonnull PhysicsExtensionSettings settings) { + entries = entriesFrom(settings); + } + + private ExtensionSettingsComponent(@Nonnull Entry[] entries) { + this.entries = copyEntries(entries); + } + + @Nonnull + public Entry[] entries() { + return copyEntries(entries); + } + + public void copyTo(@Nonnull PhysicsExtensionSettings settings) { + for (Entry entry : entries) { + entry.copyTo(settings); + } + } + + public boolean isDefault() { + return entries.length == 0; + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.extensionSettingsComponentType(); + } + + @Nonnull + @Override + public ExtensionSettingsComponent clone() { + return new ExtensionSettingsComponent(entries); + } + + @Nonnull + private static Entry[] entriesFrom(@Nonnull PhysicsExtensionSettings settings) { + return settings.asMap().entrySet().stream() + .flatMap(entry -> entry.getValue().entrySet().stream() + .map(setting -> new Entry(entry.getKey().value(), + setting.getKey(), + setting.getValue().kind(), + setting.getValue().value()))) + .toArray(Entry[]::new); + } + + @Nonnull + private static Entry[] copyEntries(Entry[] entries) { + if (entries == null || entries.length == 0) { + return EMPTY_ENTRIES; + } + return Arrays.stream(entries) + .map(Entry::clone) + .toArray(Entry[]::new); + } + + @Nonnull + public Map> asMap() { + PhysicsExtensionSettings settings = new PhysicsExtensionSettings(); + copyTo(settings); + return settings.asMap(); + } + + /** + * One capability-keyed extension setting. + */ + public static final class Entry { + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder(Entry.class, + Entry::new) + .append(new KeyedCodec<>("CapabilityId", Codec.STRING), + (entry, value) -> entry.capabilityId = value, + Entry::capabilityId) + .add() + .append(new KeyedCodec<>("Key", Codec.STRING), + (entry, value) -> entry.key = value, + Entry::key) + .add() + .append(new KeyedCodec<>("Kind", + new EnumCodec<>(PhysicsExtensionSettingValue.Kind.class)), + (entry, value) -> entry.kind = value, + Entry::kind) + .add() + .append(new KeyedCodec<>("Value", Codec.STRING), + (entry, value) -> entry.value = value, + Entry::value) + .add() + .build(); + + @Nonnull + private String capabilityId = ""; + @Nonnull + private String key = ""; + @Nonnull + private PhysicsExtensionSettingValue.Kind kind = + PhysicsExtensionSettingValue.Kind.STRING; + @Nonnull + private String value = ""; + + public Entry() { + } + + private Entry(@Nonnull String capabilityId, + @Nonnull String key, + @Nonnull PhysicsExtensionSettingValue.Kind kind, + @Nonnull String value) { + this.capabilityId = Objects.requireNonNull(capabilityId, "capabilityId"); + this.key = Objects.requireNonNull(key, "key"); + this.kind = Objects.requireNonNull(kind, "kind"); + this.value = Objects.requireNonNull(value, "value"); + } + + @Nonnull + public String capabilityId() { + return capabilityId; + } + + @Nonnull + public String key() { + return key; + } + + @Nonnull + public PhysicsExtensionSettingValue.Kind kind() { + return kind; + } + + @Nonnull + public String value() { + return value; + } + + private void copyTo(@Nonnull PhysicsExtensionSettings settings) { + settings.set(new PhysicsBackendExtensionId(capabilityId), + key, + new PhysicsExtensionSettingValue(kind, value)); + } + + @Nonnull + @Override + public Entry clone() { + return new Entry(capabilityId, key, kind, value); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/JointComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/JointComponent.java new file mode 100644 index 00000000..881d5e3c --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/JointComponent.java @@ -0,0 +1,271 @@ +package dev.hytalemodding.impulse.core.plugin.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.codec.codecs.EnumCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.math.vector.Vector3fUtil; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import lombok.Getter; +import lombok.Setter; +import org.joml.Vector3f; + +/** + * Authored joint entity keyed by durable endpoint body UUIDs. + */ +public final class JointComponent implements Component { + + private static final Vector3f ZERO = new Vector3f(); + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + JointComponent.class, + JointComponent::new) + .append(new KeyedCodec<>("SpaceUuid", Codec.UUID_BINARY, false), + (component, value) -> component.spaceUuid = value, + JointComponent::getSpaceUuid) + .add() + .append(new KeyedCodec<>("BodyAUuid", Codec.UUID_BINARY, false), + (component, value) -> component.bodyAUuid = value, + JointComponent::getBodyAUuid) + .add() + .append(new KeyedCodec<>("BodyBUuid", Codec.UUID_BINARY, false), + (component, value) -> component.bodyBUuid = value, + JointComponent::getBodyBUuid) + .add() + .append(new KeyedCodec<>("Type", new EnumCodec<>(JointType.class), false), + (component, value) -> component.type = value != null ? value : JointType.FIXED, + JointComponent::getType) + .add() + .append(new KeyedCodec<>("AnchorA", Vector3fUtil.CODEC, false), + (component, value) -> component.anchorA.set(value != null ? value : ZERO), + JointComponent::getAnchorA) + .add() + .append(new KeyedCodec<>("AnchorB", Vector3fUtil.CODEC, false), + (component, value) -> component.anchorB.set(value != null ? value : ZERO), + JointComponent::getAnchorB) + .add() + .append(new KeyedCodec<>("Axis", Vector3fUtil.CODEC, false), + (component, value) -> component.axis.set(value != null ? value : ZERO), + JointComponent::getAxis) + .add() + .append(new KeyedCodec<>("LowerLimit", Codec.FLOAT, false), + (component, value) -> component.lowerLimit = value != null ? value : 0.0f, + JointComponent::getLowerLimit) + .add() + .append(new KeyedCodec<>("UpperLimit", Codec.FLOAT, false), + (component, value) -> component.upperLimit = value != null ? value : 0.0f, + JointComponent::getUpperLimit) + .add() + .append(new KeyedCodec<>("Enabled", Codec.BOOLEAN, false), + (component, value) -> component.enabled = value == null || value, + JointComponent::isEnabled) + .add() + .append(new KeyedCodec<>("MotorEnabled", Codec.BOOLEAN, false), + (component, value) -> component.motorEnabled = value != null && value, + JointComponent::isMotorEnabled) + .add() + .append(new KeyedCodec<>("MotorTargetVelocity", Codec.FLOAT, false), + (component, value) -> component.motorTargetVelocity = value != null ? value : 0.0f, + JointComponent::getMotorTargetVelocity) + .add() + .append(new KeyedCodec<>("MotorMaxForce", Codec.FLOAT, false), + (component, value) -> component.motorMaxForce = value != null ? value : 0.0f, + JointComponent::getMotorMaxForce) + .add() + .append(new KeyedCodec<>("SpringRestLength", Codec.FLOAT, false), + (component, value) -> component.springRestLength = value != null ? value : 0.0f, + JointComponent::getSpringRestLength) + .add() + .append(new KeyedCodec<>("SpringStiffness", Codec.FLOAT, false), + (component, value) -> component.springStiffness = value != null ? value : 0.0f, + JointComponent::getSpringStiffness) + .add() + .append(new KeyedCodec<>("SpringDamping", Codec.FLOAT, false), + (component, value) -> component.springDamping = value != null ? value : 0.0f, + JointComponent::getSpringDamping) + .add() + .build(); + + @Nonnull + private UUID spaceUuid = new UUID(0L, 0L); + @Nonnull + private UUID bodyAUuid = new UUID(0L, 0L); + @Nonnull + private UUID bodyBUuid = new UUID(0L, 0L); + @Nullable + private transient Ref spaceRef; + @Nullable + private transient Ref bodyARef; + @Nullable + private transient Ref bodyBRef; + @Nonnull + private JointType type = JointType.FIXED; + @Nonnull + private final Vector3f anchorA = new Vector3f(); + @Nonnull + private final Vector3f anchorB = new Vector3f(); + @Nonnull + private final Vector3f axis = new Vector3f(); + @Setter + @Getter + private float lowerLimit; + @Setter + @Getter + private float upperLimit; + @Setter + @Getter + private boolean enabled = true; + @Setter + @Getter + private boolean motorEnabled; + @Setter + @Getter + private float motorTargetVelocity; + @Setter + @Getter + private float motorMaxForce; + @Setter + @Getter + private float springRestLength; + @Setter + @Getter + private float springStiffness; + @Setter + @Getter + private float springDamping; + + public JointComponent() { + } + + @Nonnull + public UUID getSpaceUuid() { + return spaceUuid; + } + + public void setSpaceUuid(@Nonnull UUID spaceUuid) { + this.spaceUuid = Objects.requireNonNull(spaceUuid, "spaceUuid"); + this.spaceRef = null; + } + + @Nullable + public Ref getSpaceRef() { + return spaceRef; + } + + public void setSpaceRef(@Nullable Ref spaceRef) { + this.spaceRef = spaceRef; + } + + @Nonnull + public UUID getBodyAUuid() { + return bodyAUuid; + } + + public void setBodyAUuid(@Nonnull UUID bodyAUuid) { + this.bodyAUuid = Objects.requireNonNull(bodyAUuid, "bodyAUuid"); + this.bodyARef = null; + } + + @Nullable + public Ref getBodyARef() { + return bodyARef; + } + + public void setBodyARef(@Nullable Ref bodyARef) { + this.bodyARef = bodyARef; + } + + @Nonnull + public UUID getBodyBUuid() { + return bodyBUuid; + } + + public void setBodyBUuid(@Nonnull UUID bodyBUuid) { + this.bodyBUuid = Objects.requireNonNull(bodyBUuid, "bodyBUuid"); + this.bodyBRef = null; + } + + @Nullable + public Ref getBodyBRef() { + return bodyBRef; + } + + public void setBodyBRef(@Nullable Ref bodyBRef) { + this.bodyBRef = bodyBRef; + } + + @Nonnull + public JointType getType() { + return type; + } + + public void setType(@Nonnull JointType type) { + this.type = Objects.requireNonNull(type, "type"); + } + + @Nonnull + public Vector3f getAnchorA() { + return new Vector3f(anchorA); + } + + public void setAnchorA(@Nonnull Vector3f anchorA) { + this.anchorA.set(Objects.requireNonNull(anchorA, "anchorA")); + } + + @Nonnull + public Vector3f getAnchorB() { + return new Vector3f(anchorB); + } + + public void setAnchorB(@Nonnull Vector3f anchorB) { + this.anchorB.set(Objects.requireNonNull(anchorB, "anchorB")); + } + + @Nonnull + public Vector3f getAxis() { + return new Vector3f(axis); + } + + public void setAxis(@Nonnull Vector3f axis) { + this.axis.set(Objects.requireNonNull(axis, "axis")); + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.jointComponentType(); + } + + @Nonnull + @Override + public JointComponent clone() { + JointComponent copy = new JointComponent(); + copy.spaceUuid = spaceUuid; + copy.bodyAUuid = bodyAUuid; + copy.bodyBUuid = bodyBUuid; + copy.spaceRef = spaceRef; + copy.bodyARef = bodyARef; + copy.bodyBRef = bodyBRef; + copy.type = type; + copy.anchorA.set(anchorA); + copy.anchorB.set(anchorB); + copy.axis.set(axis); + copy.lowerLimit = lowerLimit; + copy.upperLimit = upperLimit; + copy.enabled = enabled; + copy.motorEnabled = motorEnabled; + copy.motorTargetVelocity = motorTargetVelocity; + copy.motorMaxForce = motorMaxForce; + copy.springRestLength = springRestLength; + copy.springStiffness = springStiffness; + copy.springDamping = springDamping; + return copy; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/JointType.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/JointType.java new file mode 100644 index 00000000..a7060e9d --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/JointType.java @@ -0,0 +1,12 @@ +package dev.hytalemodding.impulse.core.plugin.components; + +/** + * Public joint kinds supported by PhysicsStore joint entities and snapshot views. + */ +public enum JointType { + FIXED, + POINT, + HINGE, + SLIDER, + SPRING +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/MaterialComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/MaterialComponent.java new file mode 100644 index 00000000..65cf683f --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/MaterialComponent.java @@ -0,0 +1,55 @@ +package dev.hytalemodding.impulse.core.plugin.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import javax.annotation.Nonnull; +import lombok.Getter; +import lombok.Setter; + +/** + * Physical material settings for one physics row that authors collider material. + */ +@Setter +@Getter +public final class MaterialComponent implements Component { + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + MaterialComponent.class, + MaterialComponent::new) + .append(new KeyedCodec<>("Friction", Codec.FLOAT, false), + (component, value) -> component.friction = value != null ? value : 0.5f, + MaterialComponent::getFriction) + .add() + .append(new KeyedCodec<>("Restitution", Codec.FLOAT, false), + (component, value) -> component.restitution = value != null ? value : 0.0f, + MaterialComponent::getRestitution) + .add() + .build(); + + private float friction = 0.5f; + private float restitution; + + public MaterialComponent() { + } + + public MaterialComponent(float friction, float restitution) { + this.friction = friction; + this.restitution = restitution; + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.materialComponentType(); + } + + @Nonnull + @Override + public MaterialComponent clone() { + return new MaterialComponent(friction, restitution); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyCollisionComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyCollisionComponent.java deleted file mode 100644 index fec3b99f..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyCollisionComponent.java +++ /dev/null @@ -1,70 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.components; - -import com.hypixel.hytale.codec.Codec; -import com.hypixel.hytale.codec.KeyedCodec; -import com.hypixel.hytale.codec.builder.BuilderCodec; -import com.hypixel.hytale.component.Component; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import javax.annotation.Nonnull; -import lombok.Getter; -import lombok.Setter; - -/** - * Durable ECS collision filtering data for an entity-authored physics body. - */ -public class PhysicsBodyCollisionComponent implements Component { - - @Nonnull - public static final BuilderCodec CODEC = BuilderCodec.builder( - PhysicsBodyCollisionComponent.class, - PhysicsBodyCollisionComponent::new) - .append(new KeyedCodec<>("Sensor", Codec.BOOLEAN, false), - (component, value) -> component.sensor = value != null && value, - PhysicsBodyCollisionComponent::isSensor) - .add() - .append(new KeyedCodec<>("CollisionGroup", Codec.INTEGER, false), - (component, value) -> component.collisionGroup = value != null - ? value - : PhysicsCollisionFilters.DYNAMIC_BODY, - PhysicsBodyCollisionComponent::getCollisionGroup) - .add() - .append(new KeyedCodec<>("CollisionMask", Codec.INTEGER, false), - (component, value) -> component.collisionMask = value != null - ? value - : PhysicsCollisionFilters.ALL, - PhysicsBodyCollisionComponent::getCollisionMask) - .add() - .build(); - - @Setter - @Getter - private boolean sensor; - @Setter - @Getter - private int collisionGroup = PhysicsCollisionFilters.DYNAMIC_BODY; - @Setter - @Getter - private int collisionMask = PhysicsCollisionFilters.ALL; - - public PhysicsBodyCollisionComponent() { - } - - public PhysicsBodyCollisionComponent(boolean sensor, int collisionGroup, int collisionMask) { - this.sensor = sensor; - this.collisionGroup = collisionGroup; - this.collisionMask = collisionMask; - } - - public static ComponentType getComponentType() { - return ImpulsePlugin.get().getPhysicsBodyCollisionComponentType(); - } - - @Nonnull - @Override - public PhysicsBodyCollisionComponent clone() { - return new PhysicsBodyCollisionComponent(sensor, collisionGroup, collisionMask); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyComponentValues.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyComponentValues.java deleted file mode 100644 index 85aacacb..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyComponentValues.java +++ /dev/null @@ -1,65 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.components; - -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import java.util.Objects; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Stateless conversions from ECS body authoring components to simulation values. - */ -public final class PhysicsBodyComponentValues { - - private PhysicsBodyComponentValues() { - } - - public static boolean hasExplicitSpace(@Nullable PhysicsBodyIdentityComponent component) { - return component != null - && component.getSpaceId() != null - && component.getSpaceId().value() > 0; - } - - @Nonnull - public static PhysicsShapeSpec toShapeSpec(@Nonnull PhysicsBodyShapeComponent component) { - Objects.requireNonNull(component, "component"); - return switch (component.getShapeType()) { - case BOX -> PhysicsShapeSpec.box(component.getHalfExtentX(), - component.getHalfExtentY(), - component.getHalfExtentZ()); - case SPHERE -> PhysicsShapeSpec.sphere(component.getRadius()); - case CAPSULE -> PhysicsShapeSpec.capsule(component.getRadius(), - component.getHalfHeight(), - component.getAxis()); - case CYLINDER -> PhysicsShapeSpec.cylinder(component.getRadius(), - component.getHalfHeight(), - component.getAxis()); - case CONE -> PhysicsShapeSpec.cone(component.getRadius(), - component.getHalfHeight(), - component.getAxis()); - case PLANE -> PhysicsShapeSpec.plane(component.getGroundY()); - case VOXELS, UNKNOWN -> - throw new IllegalArgumentException("Unsupported physics body shape " - + component.getShapeType()); - }; - } - - @Nonnull - public static RigidBodySpawnSettings toSpawnSettings( - @Nonnull PhysicsBodyDynamicsComponent dynamics, - @Nonnull PhysicsBodyMaterialComponent material, - @Nonnull PhysicsBodyCollisionComponent collision) { - Objects.requireNonNull(dynamics, "dynamics"); - Objects.requireNonNull(material, "material"); - Objects.requireNonNull(collision, "collision"); - return RigidBodySpawnSettings.fromOptionalValues( - material.getFriction(), - material.getRestitution(), - dynamics.getLinearDamping(), - dynamics.getAngularDamping(), - collision.getCollisionGroup(), - collision.getCollisionMask(), - collision.isSensor()); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyDynamicsComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyDynamicsComponent.java deleted file mode 100644 index 27e1d619..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyDynamicsComponent.java +++ /dev/null @@ -1,89 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.components; - -import com.hypixel.hytale.codec.Codec; -import com.hypixel.hytale.codec.KeyedCodec; -import com.hypixel.hytale.codec.builder.BuilderCodec; -import com.hypixel.hytale.codec.codecs.EnumCodec; -import com.hypixel.hytale.component.Component; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import java.util.Objects; -import javax.annotation.Nonnull; -import lombok.Getter; -import lombok.Setter; - -/** - * Durable ECS mass and damping data for an entity-authored physics body. - */ -public class PhysicsBodyDynamicsComponent implements Component { - - @Nonnull - public static final BuilderCodec CODEC = BuilderCodec.builder( - PhysicsBodyDynamicsComponent.class, - PhysicsBodyDynamicsComponent::new) - .append(new KeyedCodec<>("BodyType", new EnumCodec<>(PhysicsBodyType.class), false), - (component, value) -> component.bodyType = value != null - ? value - : PhysicsBodyType.DYNAMIC, - PhysicsBodyDynamicsComponent::getBodyType) - .add() - .append(new KeyedCodec<>("Mass", Codec.FLOAT, false), - (component, value) -> component.mass = value != null ? value : 1.0f, - PhysicsBodyDynamicsComponent::getMass) - .add() - .append(new KeyedCodec<>("LinearDamping", Codec.FLOAT, false), - (component, value) -> component.linearDamping = value != null ? value : 0.0f, - PhysicsBodyDynamicsComponent::getLinearDamping) - .add() - .append(new KeyedCodec<>("AngularDamping", Codec.FLOAT, false), - (component, value) -> component.angularDamping = value != null ? value : 0.0f, - PhysicsBodyDynamicsComponent::getAngularDamping) - .add() - .build(); - - @Nonnull - private PhysicsBodyType bodyType = PhysicsBodyType.DYNAMIC; - @Setter - @Getter - private float mass = 1.0f; - @Setter - @Getter - private float linearDamping; - @Setter - @Getter - private float angularDamping; - - public PhysicsBodyDynamicsComponent() { - } - - public PhysicsBodyDynamicsComponent(@Nonnull PhysicsBodyType bodyType, - float mass, - float linearDamping, - float angularDamping) { - this.bodyType = Objects.requireNonNull(bodyType, "bodyType"); - this.mass = mass; - this.linearDamping = linearDamping; - this.angularDamping = angularDamping; - } - - @Nonnull - public PhysicsBodyType getBodyType() { - return bodyType; - } - - public void setBodyType(@Nonnull PhysicsBodyType bodyType) { - this.bodyType = Objects.requireNonNull(bodyType, "bodyType"); - } - - public static ComponentType getComponentType() { - return ImpulsePlugin.get().getPhysicsBodyDynamicsComponentType(); - } - - @Nonnull - @Override - public PhysicsBodyDynamicsComponent clone() { - return new PhysicsBodyDynamicsComponent(bodyType, mass, linearDamping, angularDamping); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyIdentityComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyIdentityComponent.java deleted file mode 100644 index 7bb395f5..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyIdentityComponent.java +++ /dev/null @@ -1,112 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.components; - -import com.hypixel.hytale.codec.Codec; -import com.hypixel.hytale.codec.KeyedCodec; -import com.hypixel.hytale.codec.builder.BuilderCodec; -import com.hypixel.hytale.codec.codecs.EnumCodec; -import com.hypixel.hytale.component.Component; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import java.util.Objects; -import java.util.UUID; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Durable ECS identity for an entity-authored physics body. - */ -public class PhysicsBodyIdentityComponent implements Component { - - @Nonnull - public static final BuilderCodec CODEC = BuilderCodec.builder( - PhysicsBodyIdentityComponent.class, - PhysicsBodyIdentityComponent::new) - .append(new KeyedCodec<>("BodyId", Codec.UUID_BINARY, false), - (component, value) -> component.bodyKey = value != null - ? RigidBodyKey.of(value) - : RigidBodyKey.random(), - PhysicsBodyIdentityComponent::getBodyKeyValue) - .add() - .append(new KeyedCodec<>("SpaceId", Codec.INTEGER, false), - (component, value) -> component.spaceId = value != null && value > 0 - ? new SpaceId(value) - : null, - PhysicsBodyIdentityComponent::getSpaceIdValue) - .add() - .append(new KeyedCodec<>("PersistenceMode", new EnumCodec<>(PhysicsBodyPersistenceMode.class), false), - (component, value) -> component.persistenceMode = value != null - ? value - : PhysicsBodyPersistenceMode.RUNTIME_ONLY, - PhysicsBodyIdentityComponent::getPersistenceMode) - .add() - .build(); - - @Nonnull - private RigidBodyKey bodyKey = RigidBodyKey.random(); - @Nullable - private SpaceId spaceId; - @Nonnull - private PhysicsBodyPersistenceMode persistenceMode = PhysicsBodyPersistenceMode.RUNTIME_ONLY; - - public PhysicsBodyIdentityComponent() { - } - - public PhysicsBodyIdentityComponent(@Nonnull RigidBodyKey bodyKey, - @Nullable SpaceId spaceId, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - this.bodyKey = Objects.requireNonNull(bodyKey, "bodyKey"); - this.spaceId = spaceId; - this.persistenceMode = Objects.requireNonNull(persistenceMode, "persistenceMode"); - } - - @Nonnull - public RigidBodyKey getBodyKey() { - return bodyKey; - } - - public void setBodyKey(@Nonnull RigidBodyKey bodyKey) { - this.bodyKey = Objects.requireNonNull(bodyKey, "bodyKey"); - } - - @Nullable - public SpaceId getSpaceId() { - return spaceId; - } - - public void setSpaceId(@Nullable SpaceId spaceId) { - this.spaceId = spaceId; - } - - @Nonnull - public PhysicsBodyPersistenceMode getPersistenceMode() { - return persistenceMode; - } - - public void setPersistenceMode(@Nonnull PhysicsBodyPersistenceMode persistenceMode) { - this.persistenceMode = Objects.requireNonNull(persistenceMode, "persistenceMode"); - } - - public static ComponentType getComponentType() { - return ImpulsePlugin.get().getPhysicsBodyIdentityComponentType(); - } - - @Nullable - private Integer getSpaceIdValue() { - return spaceId != null ? spaceId.value() : null; - } - - @Nonnull - private UUID getBodyKeyValue() { - return bodyKey.value(); - } - - @Nonnull - @Override - public PhysicsBodyIdentityComponent clone() { - return new PhysicsBodyIdentityComponent(bodyKey, spaceId, persistenceMode); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyKinematicTargetComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyKinematicTargetComponent.java deleted file mode 100644 index ef34bdc1..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyKinematicTargetComponent.java +++ /dev/null @@ -1,130 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.components; - -import com.hypixel.hytale.codec.Codec; -import com.hypixel.hytale.codec.KeyedCodec; -import com.hypixel.hytale.codec.builder.BuilderCodec; -import com.hypixel.hytale.component.Component; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.math.vector.Vector3fUtil; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.plugin.codec.ImpulseCodecs; -import java.util.Objects; -import javax.annotation.Nonnull; -import lombok.Getter; -import lombok.Setter; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -/** - * Data-only kinematic target for an entity-authored physics body. - */ -public class PhysicsBodyKinematicTargetComponent implements Component { - - @Nonnull - public static final BuilderCodec CODEC = BuilderCodec.builder( - PhysicsBodyKinematicTargetComponent.class, - PhysicsBodyKinematicTargetComponent::new) - .append(new KeyedCodec<>("Position", Vector3fUtil.CODEC, false), - (component, value) -> component.position.set(value != null ? value : new Vector3f()), - PhysicsBodyKinematicTargetComponent::getPosition) - .add() - .append(new KeyedCodec<>("Rotation", ImpulseCodecs.QUATERNIONF, false), - (component, value) -> component.rotation.set(value != null ? value : new Quaternionf()), - component -> new Quaternionf(component.rotation)) - .add() - .append(new KeyedCodec<>("LinearVelocity", Vector3fUtil.CODEC, false), - (component, value) -> component.linearVelocity.set(value != null ? value : new Vector3f()), - PhysicsBodyKinematicTargetComponent::getLinearVelocity) - .add() - .append(new KeyedCodec<>("AngularVelocity", Vector3fUtil.CODEC, false), - (component, value) -> component.angularVelocity.set(value != null ? value : new Vector3f()), - PhysicsBodyKinematicTargetComponent::getAngularVelocity) - .add() - .append(new KeyedCodec<>("TransformEnabled", Codec.BOOLEAN, false), - (component, value) -> component.transformEnabled = value == null || value, - PhysicsBodyKinematicTargetComponent::isTransformEnabled) - .add() - .append(new KeyedCodec<>("VelocityEnabled", Codec.BOOLEAN, false), - (component, value) -> component.velocityEnabled = value != null && value, - PhysicsBodyKinematicTargetComponent::isVelocityEnabled) - .add() - .append(new KeyedCodec<>("Activate", Codec.BOOLEAN, false), - (component, value) -> component.activate = value == null || value, - PhysicsBodyKinematicTargetComponent::isActivate) - .add() - .build(); - - @Nonnull - private final Vector3f position = new Vector3f(); - @Nonnull - private final Quaternionf rotation = new Quaternionf(); - @Nonnull - private final Vector3f linearVelocity = new Vector3f(); - @Nonnull - private final Vector3f angularVelocity = new Vector3f(); - @Setter - @Getter - private boolean transformEnabled = true; - @Setter - @Getter - private boolean velocityEnabled; - @Setter - @Getter - private boolean activate = true; - - public PhysicsBodyKinematicTargetComponent() { - } - - public PhysicsBodyKinematicTargetComponent(@Nonnull Vector3f position, - @Nonnull Quaternionf rotation, - @Nonnull Vector3f linearVelocity, - @Nonnull Vector3f angularVelocity, - boolean transformEnabled, - boolean velocityEnabled, - boolean activate) { - this.position.set(Objects.requireNonNull(position, "position")); - this.rotation.set(Objects.requireNonNull(rotation, "rotation")); - this.linearVelocity.set(Objects.requireNonNull(linearVelocity, "linearVelocity")); - this.angularVelocity.set(Objects.requireNonNull(angularVelocity, "angularVelocity")); - this.transformEnabled = transformEnabled; - this.velocityEnabled = velocityEnabled; - this.activate = activate; - } - - @Nonnull - public Vector3f getPosition() { - return position; - } - - @Nonnull - public Quaternionf getRotation() { - return rotation; - } - - @Nonnull - public Vector3f getLinearVelocity() { - return linearVelocity; - } - - @Nonnull - public Vector3f getAngularVelocity() { - return angularVelocity; - } - - public static ComponentType getComponentType() { - return ImpulsePlugin.get().getPhysicsBodyKinematicTargetComponentType(); - } - - @Nonnull - @Override - public PhysicsBodyKinematicTargetComponent clone() { - return new PhysicsBodyKinematicTargetComponent(position, - rotation, - linearVelocity, - angularVelocity, - transformEnabled, - velocityEnabled, - activate); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyLifecycleComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyLifecycleComponent.java deleted file mode 100644 index a40d7dc9..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyLifecycleComponent.java +++ /dev/null @@ -1,127 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.components; - -import com.hypixel.hytale.codec.Codec; -import com.hypixel.hytale.codec.KeyedCodec; -import com.hypixel.hytale.codec.builder.BuilderCodec; -import com.hypixel.hytale.codec.codecs.EnumCodec; -import com.hypixel.hytale.component.Component; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import java.util.Objects; -import java.util.UUID; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Runtime readback state for entity-authored physics body reconciliation. - */ -public class PhysicsBodyLifecycleComponent implements Component { - - @Nonnull - public static final BuilderCodec CODEC = BuilderCodec.builder( - PhysicsBodyLifecycleComponent.class, - PhysicsBodyLifecycleComponent::new) - .append(new KeyedCodec<>("State", new EnumCodec<>(State.class), false), - (component, value) -> component.state = value != null ? value : State.PENDING, - PhysicsBodyLifecycleComponent::getState) - .add() - .append(new KeyedCodec<>("BodyId", Codec.UUID_BINARY, false), - (component, value) -> component.bodyKey = value != null ? RigidBodyKey.of(value) : null, - PhysicsBodyLifecycleComponent::getBodyKeyValue) - .add() - .append(new KeyedCodec<>("Message", Codec.STRING, false), - (component, value) -> component.message = value, - PhysicsBodyLifecycleComponent::getMessage) - .add() - .build(); - - @Nonnull - private State state = State.PENDING; - @Nullable - private RigidBodyKey bodyKey; - @Nullable - private String message; - - public PhysicsBodyLifecycleComponent() { - } - - public PhysicsBodyLifecycleComponent(@Nonnull State state, - @Nullable RigidBodyKey bodyKey, - @Nullable String message) { - this.state = Objects.requireNonNull(state, "state"); - this.bodyKey = bodyKey; - this.message = message; - } - - @Nonnull - public static PhysicsBodyLifecycleComponent pending(@Nonnull RigidBodyKey bodyKey) { - return new PhysicsBodyLifecycleComponent(State.PENDING, bodyKey, null); - } - - @Nonnull - public static PhysicsBodyLifecycleComponent created(@Nonnull RigidBodyKey bodyKey) { - return new PhysicsBodyLifecycleComponent(State.CREATED, bodyKey, null); - } - - @Nonnull - public static PhysicsBodyLifecycleComponent destroyed(@Nonnull RigidBodyKey bodyKey) { - return new PhysicsBodyLifecycleComponent(State.DESTROYED, bodyKey, null); - } - - @Nonnull - public static PhysicsBodyLifecycleComponent failed(@Nullable RigidBodyKey bodyKey, - @Nonnull String message) { - return new PhysicsBodyLifecycleComponent(State.FAILED, bodyKey, message); - } - - @Nonnull - public State getState() { - return state; - } - - public void setState(@Nonnull State state) { - this.state = Objects.requireNonNull(state, "state"); - } - - @Nullable - public RigidBodyKey getBodyKey() { - return bodyKey; - } - - public void setBodyKey(@Nullable RigidBodyKey bodyKey) { - this.bodyKey = bodyKey; - } - - @Nullable - public String getMessage() { - return message; - } - - public void setMessage(@Nullable String message) { - this.message = message; - } - - public static ComponentType getComponentType() { - return ImpulsePlugin.get().getPhysicsBodyLifecycleComponentType(); - } - - @Nullable - private UUID getBodyKeyValue() { - return bodyKey != null ? bodyKey.value() : null; - } - - @Nonnull - @Override - public PhysicsBodyLifecycleComponent clone() { - return new PhysicsBodyLifecycleComponent(state, bodyKey, message); - } - - public enum State { - PENDING, - CREATED, - DESTROYED, - FAILED - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyMaterialComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyMaterialComponent.java deleted file mode 100644 index 2fca5533..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyMaterialComponent.java +++ /dev/null @@ -1,57 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.components; - -import com.hypixel.hytale.codec.Codec; -import com.hypixel.hytale.codec.KeyedCodec; -import com.hypixel.hytale.codec.builder.BuilderCodec; -import com.hypixel.hytale.component.Component; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import javax.annotation.Nonnull; -import lombok.Getter; -import lombok.Setter; - -/** - * Durable ECS material data for an entity-authored physics body. - */ -public class PhysicsBodyMaterialComponent implements Component { - - @Nonnull - public static final BuilderCodec CODEC = BuilderCodec.builder( - PhysicsBodyMaterialComponent.class, - PhysicsBodyMaterialComponent::new) - .append(new KeyedCodec<>("Friction", Codec.FLOAT, false), - (component, value) -> component.friction = value != null ? value : 0.5f, - PhysicsBodyMaterialComponent::getFriction) - .add() - .append(new KeyedCodec<>("Restitution", Codec.FLOAT, false), - (component, value) -> component.restitution = value != null ? value : 0.0f, - PhysicsBodyMaterialComponent::getRestitution) - .add() - .build(); - - @Setter - @Getter - private float friction = 0.5f; - @Setter - @Getter - private float restitution; - - public PhysicsBodyMaterialComponent() { - } - - public PhysicsBodyMaterialComponent(float friction, float restitution) { - this.friction = friction; - this.restitution = restitution; - } - - public static ComponentType getComponentType() { - return ImpulsePlugin.get().getPhysicsBodyMaterialComponentType(); - } - - @Nonnull - @Override - public PhysicsBodyMaterialComponent clone() { - return new PhysicsBodyMaterialComponent(friction, restitution); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsComponentTypes.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsComponentTypes.java new file mode 100644 index 00000000..fbda5c38 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsComponentTypes.java @@ -0,0 +1,109 @@ +package dev.hytalemodding.impulse.core.plugin.components; + +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.CollisionLodSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.ChunkCollisionSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.VisualMaterializationSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.VisualSyncSettingsComponent; +import javax.annotation.Nonnull; + +/** + * Public Hytale ECS component type handles for PhysicsStore entities. + */ +public final class PhysicsComponentTypes { + + private PhysicsComponentTypes() { + } + + @Nonnull + public static ComponentType uuidComponentType() { + return PhysicsComponentTypeRegistry.uuidComponentType(); + } + + @Nonnull + public static ComponentType spaceComponentType() { + return PhysicsComponentTypeRegistry.spaceComponentType(); + } + + @Nonnull + public static ComponentType bodyComponentType() { + return PhysicsComponentTypeRegistry.bodyComponentType(); + } + + @Nonnull + public static ComponentType bodyCommandComponentType() { + return PhysicsComponentTypeRegistry.bodyCommandComponentType(); + } + + @Nonnull + public static ComponentType + chunkCollisionSettingsComponentType() { + return PhysicsComponentTypeRegistry.chunkCollisionSettingsComponentType(); + } + + @Nonnull + public static ComponentType dynamicsComponentType() { + return PhysicsComponentTypeRegistry.dynamicsComponentType(); + } + + @Nonnull + public static ComponentType colliderComponentType() { + return PhysicsComponentTypeRegistry.colliderComponentType(); + } + + @Nonnull + public static ComponentType shapeComponentType() { + return PhysicsComponentTypeRegistry.shapeComponentType(); + } + + @Nonnull + public static ComponentType materialComponentType() { + return PhysicsComponentTypeRegistry.materialComponentType(); + } + + @Nonnull + public static ComponentType collisionFilterComponentType() { + return PhysicsComponentTypeRegistry.collisionFilterComponentType(); + } + + @Nonnull + public static ComponentType jointComponentType() { + return PhysicsComponentTypeRegistry.jointComponentType(); + } + + @Nonnull + public static ComponentType targetComponentType() { + return PhysicsComponentTypeRegistry.targetComponentType(); + } + + @Nonnull + public static ComponentType solverSettingsComponentType() { + return PhysicsComponentTypeRegistry.solverSettingsComponentType(); + } + + @Nonnull + public static ComponentType + visualSyncSettingsComponentType() { + return PhysicsComponentTypeRegistry.visualSyncSettingsComponentType(); + } + + @Nonnull + public static ComponentType + visualMaterializationSettingsComponentType() { + return PhysicsComponentTypeRegistry.visualMaterializationSettingsComponentType(); + } + + @Nonnull + public static ComponentType + collisionLodSettingsComponentType() { + return PhysicsComponentTypeRegistry.collisionLodSettingsComponentType(); + } + + @Nonnull + public static ComponentType + extensionSettingsComponentType() { + return PhysicsComponentTypeRegistry.extensionSettingsComponentType(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyShapeComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/ShapeComponent.java similarity index 62% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyShapeComponent.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/ShapeComponent.java index 65861b4a..d7e7795d 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyShapeComponent.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/ShapeComponent.java @@ -6,55 +6,58 @@ import com.hypixel.hytale.codec.codecs.EnumCodec; import com.hypixel.hytale.component.Component; import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.PhysicsAxis; import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import java.util.Objects; -import javax.annotation.Nonnull; import lombok.Getter; import lombok.Setter; +import java.util.Objects; +import javax.annotation.Nonnull; /** - * Durable ECS shape data for an entity-authored physics body. + * Authored collision shape for one collider entity. */ -public class PhysicsBodyShapeComponent implements Component { +public final class ShapeComponent implements Component { @Nonnull - public static final BuilderCodec CODEC = BuilderCodec.builder( - PhysicsBodyShapeComponent.class, - PhysicsBodyShapeComponent::new) + public static final BuilderCodec CODEC = BuilderCodec.builder( + ShapeComponent.class, + ShapeComponent::new) .append(new KeyedCodec<>("ShapeType", new EnumCodec<>(ShapeType.class), false), (component, value) -> component.shapeType = value != null ? value : ShapeType.BOX, - PhysicsBodyShapeComponent::getShapeType) + ShapeComponent::getShapeType) .add() .append(new KeyedCodec<>("HalfExtentX", Codec.FLOAT, false), (component, value) -> component.halfExtentX = value != null ? value : 0.5f, - PhysicsBodyShapeComponent::getHalfExtentX) + ShapeComponent::getHalfExtentX) .add() .append(new KeyedCodec<>("HalfExtentY", Codec.FLOAT, false), (component, value) -> component.halfExtentY = value != null ? value : 0.5f, - PhysicsBodyShapeComponent::getHalfExtentY) + ShapeComponent::getHalfExtentY) .add() .append(new KeyedCodec<>("HalfExtentZ", Codec.FLOAT, false), (component, value) -> component.halfExtentZ = value != null ? value : 0.5f, - PhysicsBodyShapeComponent::getHalfExtentZ) + ShapeComponent::getHalfExtentZ) .add() .append(new KeyedCodec<>("Radius", Codec.FLOAT, false), (component, value) -> component.radius = value != null ? value : 0.5f, - PhysicsBodyShapeComponent::getRadius) + ShapeComponent::getRadius) .add() .append(new KeyedCodec<>("HalfHeight", Codec.FLOAT, false), (component, value) -> component.halfHeight = value != null ? value : 0.5f, - PhysicsBodyShapeComponent::getHalfHeight) + ShapeComponent::getHalfHeight) .add() .append(new KeyedCodec<>("Axis", new EnumCodec<>(PhysicsAxis.class), false), (component, value) -> component.axis = value != null ? value : PhysicsAxis.Y, - PhysicsBodyShapeComponent::getAxis) + ShapeComponent::getAxis) .add() .append(new KeyedCodec<>("GroundY", Codec.FLOAT, false), (component, value) -> component.groundY = value != null ? value : 0.0f, - PhysicsBodyShapeComponent::getGroundY) + ShapeComponent::getGroundY) + .add() + .append(new KeyedCodec<>("ResourceKey", Codec.STRING, false), + (component, value) -> component.resourceKey = value != null ? value : "", + ShapeComponent::getResourceKey) .add() .build(); @@ -80,18 +83,21 @@ public class PhysicsBodyShapeComponent implements Component { @Setter @Getter private float groundY; + @Nonnull + private String resourceKey = ""; - public PhysicsBodyShapeComponent() { + public ShapeComponent() { } - public PhysicsBodyShapeComponent(@Nonnull ShapeType shapeType, + public ShapeComponent(@Nonnull ShapeType shapeType, float halfExtentX, float halfExtentY, float halfExtentZ, float radius, float halfHeight, @Nonnull PhysicsAxis axis, - float groundY) { + float groundY, + @Nonnull String resourceKey) { this.shapeType = Objects.requireNonNull(shapeType, "shapeType"); this.halfExtentX = halfExtentX; this.halfExtentY = halfExtentY; @@ -100,44 +106,7 @@ public PhysicsBodyShapeComponent(@Nonnull ShapeType shapeType, this.halfHeight = halfHeight; this.axis = Objects.requireNonNull(axis, "axis"); this.groundY = groundY; - } - - @Nonnull - public static PhysicsBodyShapeComponent box(float halfX, float halfY, float halfZ) { - return new PhysicsBodyShapeComponent(ShapeType.BOX, - halfX, - halfY, - halfZ, - 0.5f, - 0.5f, - PhysicsAxis.Y, - 0.0f); - } - - @Nonnull - public static PhysicsBodyShapeComponent sphere(float radius) { - return new PhysicsBodyShapeComponent(ShapeType.SPHERE, - 0.5f, - 0.5f, - 0.5f, - radius, - 0.5f, - PhysicsAxis.Y, - 0.0f); - } - - @Nonnull - public static PhysicsBodyShapeComponent capsule(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis) { - return new PhysicsBodyShapeComponent(ShapeType.CAPSULE, - 0.5f, - 0.5f, - 0.5f, - radius, - halfHeight, - axis, - 0.0f); + this.resourceKey = Objects.requireNonNull(resourceKey, "resourceKey"); } @Nonnull @@ -158,20 +127,31 @@ public void setAxis(@Nonnull PhysicsAxis axis) { this.axis = Objects.requireNonNull(axis, "axis"); } - public static ComponentType getComponentType() { - return ImpulsePlugin.get().getPhysicsBodyShapeComponentType(); + @Nonnull + public String getResourceKey() { + return resourceKey; + } + + public void setResourceKey(@Nonnull String resourceKey) { + this.resourceKey = Objects.requireNonNull(resourceKey, "resourceKey"); + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.shapeComponentType(); } @Nonnull @Override - public PhysicsBodyShapeComponent clone() { - return new PhysicsBodyShapeComponent(shapeType, + public ShapeComponent clone() { + return new ShapeComponent(shapeType, halfExtentX, halfExtentY, halfExtentZ, radius, halfHeight, axis, - groundY); + groundY, + resourceKey); } } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/SolverSettingsComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/SolverSettingsComponent.java new file mode 100644 index 00000000..2b215778 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/SolverSettingsComponent.java @@ -0,0 +1,123 @@ +package dev.hytalemodding.impulse.core.plugin.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSolverSettings; +import lombok.Getter; +import lombok.Setter; +import javax.annotation.Nonnull; + +/** + * Authored backend solver and activation tuning for one PhysicsStore space entity. + */ +@Setter +@Getter +public final class SolverSettingsComponent implements Component { + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + SolverSettingsComponent.class, + SolverSettingsComponent::new) + .append(new KeyedCodec<>("SolverIterations", Codec.INTEGER, false), + (component, value) -> component.solverIterations = value != null + ? value + : PhysicsSolverSettings.DEFAULT_SOLVER_ITERATIONS, + SolverSettingsComponent::getSolverIterations) + .add() + .append(new KeyedCodec<>("StabilizationIterations", Codec.INTEGER, false), + (component, value) -> component.stabilizationIterations = value != null + ? value + : PhysicsSolverSettings.DEFAULT_STABILIZATION_ITERATIONS, + SolverSettingsComponent::getStabilizationIterations) + .add() + .append(new KeyedCodec<>("DynamicSleepLinearThreshold", Codec.FLOAT, false), + (component, value) -> component.dynamicSleepLinearThreshold = value != null + ? value + : PhysicsSolverSettings.DEFAULT_DYNAMIC_SLEEP_LINEAR_THRESHOLD, + SolverSettingsComponent::getDynamicSleepLinearThreshold) + .add() + .append(new KeyedCodec<>("DynamicSleepAngularThreshold", Codec.FLOAT, false), + (component, value) -> component.dynamicSleepAngularThreshold = value != null + ? value + : PhysicsSolverSettings.DEFAULT_DYNAMIC_SLEEP_ANGULAR_THRESHOLD, + SolverSettingsComponent::getDynamicSleepAngularThreshold) + .add() + .append(new KeyedCodec<>("DynamicSleepTimeUntilSleep", Codec.FLOAT, false), + (component, value) -> component.dynamicSleepTimeUntilSleep = value != null + ? value + : PhysicsSolverSettings.DEFAULT_DYNAMIC_SLEEP_TIME_UNTIL_SLEEP, + SolverSettingsComponent::getDynamicSleepTimeUntilSleep) + .add() + .build(); + + private int solverIterations = PhysicsSolverSettings.DEFAULT_SOLVER_ITERATIONS; + private int stabilizationIterations = PhysicsSolverSettings.DEFAULT_STABILIZATION_ITERATIONS; + private float dynamicSleepLinearThreshold = + PhysicsSolverSettings.DEFAULT_DYNAMIC_SLEEP_LINEAR_THRESHOLD; + private float dynamicSleepAngularThreshold = + PhysicsSolverSettings.DEFAULT_DYNAMIC_SLEEP_ANGULAR_THRESHOLD; + private float dynamicSleepTimeUntilSleep = + PhysicsSolverSettings.DEFAULT_DYNAMIC_SLEEP_TIME_UNTIL_SLEEP; + + public SolverSettingsComponent() { + } + + public SolverSettingsComponent(@Nonnull PhysicsSolverSettings settings) { + solverIterations = settings.getSolverIterations(); + stabilizationIterations = settings.getStabilizationIterations(); + dynamicSleepLinearThreshold = settings.getDynamicSleepLinearThreshold(); + dynamicSleepAngularThreshold = settings.getDynamicSleepAngularThreshold(); + dynamicSleepTimeUntilSleep = settings.getDynamicSleepTimeUntilSleep(); + } + + public SolverSettingsComponent(int solverIterations, + int stabilizationIterations, + float dynamicSleepLinearThreshold, + float dynamicSleepAngularThreshold, + float dynamicSleepTimeUntilSleep) { + this.solverIterations = solverIterations; + this.stabilizationIterations = stabilizationIterations; + this.dynamicSleepLinearThreshold = dynamicSleepLinearThreshold; + this.dynamicSleepAngularThreshold = dynamicSleepAngularThreshold; + this.dynamicSleepTimeUntilSleep = dynamicSleepTimeUntilSleep; + } + + public void copyTo(@Nonnull PhysicsSolverSettings settings) { + settings.setSolverIterations(solverIterations); + settings.setStabilizationIterations(stabilizationIterations); + settings.setDynamicSleepTuning(dynamicSleepLinearThreshold, + dynamicSleepAngularThreshold, + dynamicSleepTimeUntilSleep); + } + + public boolean isDefault() { + return solverIterations == PhysicsSolverSettings.DEFAULT_SOLVER_ITERATIONS + && stabilizationIterations + == PhysicsSolverSettings.DEFAULT_STABILIZATION_ITERATIONS + && Float.compare(dynamicSleepLinearThreshold, + PhysicsSolverSettings.DEFAULT_DYNAMIC_SLEEP_LINEAR_THRESHOLD) == 0 + && Float.compare(dynamicSleepAngularThreshold, + PhysicsSolverSettings.DEFAULT_DYNAMIC_SLEEP_ANGULAR_THRESHOLD) == 0 + && Float.compare(dynamicSleepTimeUntilSleep, + PhysicsSolverSettings.DEFAULT_DYNAMIC_SLEEP_TIME_UNTIL_SLEEP) == 0; + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.solverSettingsComponentType(); + } + + @Nonnull + @Override + public SolverSettingsComponent clone() { + return new SolverSettingsComponent(solverIterations, + stabilizationIterations, + dynamicSleepLinearThreshold, + dynamicSleepAngularThreshold, + dynamicSleepTimeUntilSleep); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/SpaceComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/SpaceComponent.java new file mode 100644 index 00000000..071a4d2e --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/SpaceComponent.java @@ -0,0 +1,84 @@ +package dev.hytalemodding.impulse.core.plugin.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.math.vector.Vector3fUtil; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.joml.Vector3f; + +/** + * Authored backend and gravity definition for one physics space entity. + */ +public final class SpaceComponent implements Component { + + public static final Vector3f DEFAULT_GRAVITY = new Vector3f(0.0f, -9.81f, 0.0f); + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + SpaceComponent.class, + SpaceComponent::new) + .append(new KeyedCodec<>("BackendId", Codec.STRING, false), + (component, value) -> component.backendId = value != null && !value.isBlank() + ? value + : "", + SpaceComponent::getBackendIdValue) + .add() + .append(new KeyedCodec<>("Gravity", Vector3fUtil.CODEC, false), + (component, value) -> component.gravity.set(value != null ? value : DEFAULT_GRAVITY), + SpaceComponent::getGravity) + .add() + .build(); + + @Nonnull + private String backendId = ""; + @Nonnull + private final Vector3f gravity = new Vector3f(DEFAULT_GRAVITY); + + public SpaceComponent() { + } + + public SpaceComponent(@Nonnull BackendId backendId, @Nonnull Vector3f gravity) { + this.backendId = Objects.requireNonNull(backendId, "backendId").value(); + this.gravity.set(Objects.requireNonNull(gravity, "gravity")); + } + + @Nonnull + public BackendId getBackendId() { + return new BackendId(backendId); + } + + public void setBackendId(@Nonnull BackendId backendId) { + this.backendId = Objects.requireNonNull(backendId, "backendId").value(); + } + + @Nonnull + public String getBackendIdValue() { + return backendId; + } + + @Nonnull + public Vector3f getGravity() { + return new Vector3f(gravity); + } + + public void setGravity(@Nonnull Vector3f gravity) { + this.gravity.set(Objects.requireNonNull(gravity, "gravity")); + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.spaceComponentType(); + } + + @Nonnull + @Override + public SpaceComponent clone() { + return new SpaceComponent(getBackendId(), gravity); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/TargetComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/TargetComponent.java new file mode 100644 index 00000000..0fff31ab --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/TargetComponent.java @@ -0,0 +1,142 @@ +package dev.hytalemodding.impulse.core.plugin.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.math.vector.Vector3fUtil; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.codec.ImpulseCodecs; +import javax.annotation.Nonnull; +import lombok.Getter; +import lombok.Setter; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +/** + * Kinematic target state applied by PhysicsStore systems to a bound body. + */ +public final class TargetComponent implements Component { + + private static final Vector3f ZERO = new Vector3f(); + private static final Quaternionf IDENTITY = new Quaternionf(); + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + TargetComponent.class, + TargetComponent::new) + .append(new KeyedCodec<>("Active", Codec.BOOLEAN, false), + (component, value) -> component.active = value != null && value, + TargetComponent::isActive) + .add() + .append(new KeyedCodec<>("Position", Vector3fUtil.CODEC, false), + (component, value) -> component.position.set(value != null ? value : ZERO), + TargetComponent::getPosition) + .add() + .append(new KeyedCodec<>("Rotation", ImpulseCodecs.QUATERNIONF, false), + (component, value) -> component.rotation.set(value != null ? value : IDENTITY), + TargetComponent::getRotation) + .add() + .append(new KeyedCodec<>("LinearVelocity", Vector3fUtil.CODEC, false), + (component, value) -> component.linearVelocity.set(value != null ? value : ZERO), + TargetComponent::getLinearVelocity) + .add() + .append(new KeyedCodec<>("AngularVelocity", Vector3fUtil.CODEC, false), + (component, value) -> component.angularVelocity.set(value != null ? value : ZERO), + TargetComponent::getAngularVelocity) + .add() + .append(new KeyedCodec<>("TransformEnabled", Codec.BOOLEAN, false), + (component, value) -> component.transformEnabled = value == null || value, + TargetComponent::isTransformEnabled) + .add() + .append(new KeyedCodec<>("VelocityEnabled", Codec.BOOLEAN, false), + (component, value) -> component.velocityEnabled = value == null || value, + TargetComponent::isVelocityEnabled) + .add() + .append(new KeyedCodec<>("Activate", Codec.BOOLEAN, false), + (component, value) -> component.activate = value == null || value, + TargetComponent::isActivate) + .add() + .build(); + + @Setter + @Getter + private boolean active; + @Getter + @Setter + private boolean transformEnabled = true; + @Getter + @Setter + private boolean velocityEnabled = true; + @Getter + @Setter + private boolean activate = true; + @Nonnull + private final Vector3f position = new Vector3f(); + @Nonnull + private final Quaternionf rotation = new Quaternionf(); + @Nonnull + private final Vector3f linearVelocity = new Vector3f(); + @Nonnull + private final Vector3f angularVelocity = new Vector3f(); + + public TargetComponent() { + } + + @Nonnull + public Vector3f getPosition() { + return new Vector3f(position); + } + + public void setPosition(@Nonnull Vector3f position) { + this.position.set(position); + } + + @Nonnull + public Quaternionf getRotation() { + return new Quaternionf(rotation); + } + + public void setRotation(@Nonnull Quaternionf rotation) { + this.rotation.set(rotation); + } + + @Nonnull + public Vector3f getLinearVelocity() { + return new Vector3f(linearVelocity); + } + + public void setLinearVelocity(@Nonnull Vector3f linearVelocity) { + this.linearVelocity.set(linearVelocity); + } + + @Nonnull + public Vector3f getAngularVelocity() { + return new Vector3f(angularVelocity); + } + + public void setAngularVelocity(@Nonnull Vector3f angularVelocity) { + this.angularVelocity.set(angularVelocity); + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.targetComponentType(); + } + + @Nonnull + @Override + public TargetComponent clone() { + TargetComponent copy = new TargetComponent(); + copy.active = active; + copy.transformEnabled = transformEnabled; + copy.velocityEnabled = velocityEnabled; + copy.activate = activate; + copy.position.set(position); + copy.rotation.set(rotation); + copy.linearVelocity.set(linearVelocity); + copy.angularVelocity.set(angularVelocity); + return copy; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/UuidComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/UuidComponent.java new file mode 100644 index 00000000..c88e4343 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/UuidComponent.java @@ -0,0 +1,57 @@ +package dev.hytalemodding.impulse.core.plugin.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; + +/** + * Durable identity for one PhysicsStore entity. + */ +public final class UuidComponent implements Component { + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + UuidComponent.class, + UuidComponent::new) + .append(new KeyedCodec<>("Uuid", Codec.UUID_BINARY, false), + (component, value) -> component.uuid = value != null ? value : UUID.randomUUID(), + UuidComponent::getUuid) + .add() + .build(); + + @Nonnull + private UUID uuid = UUID.randomUUID(); + + public UuidComponent() { + } + + public UuidComponent(@Nonnull UUID uuid) { + this.uuid = Objects.requireNonNull(uuid, "uuid"); + } + + @Nonnull + public UUID getUuid() { + return uuid; + } + + public void setUuid(@Nonnull UUID uuid) { + this.uuid = Objects.requireNonNull(uuid, "uuid"); + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.uuidComponentType(); + } + + @Nonnull + @Override + public UuidComponent clone() { + return new UuidComponent(uuid); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsBodyActivationEvent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsBodyActivationEvent.java index a42f0b3c..53a7cf45 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsBodyActivationEvent.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsBodyActivationEvent.java @@ -2,21 +2,24 @@ import dev.hytalemodding.impulse.api.PhysicsBodyActivationPhase; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; import java.util.Objects; +import java.util.UUID; import javax.annotation.Nonnull; /** * Stable body activation event copied from a backend event batch. + * + * @deprecated Physics event plugin API is deprecated without replacement. */ +@Deprecated(since = "0.1.0", forRemoval = false) public record PhysicsBodyActivationEvent(@Nonnull SpaceId spaceId, @Nonnull PhysicsBodyActivationPhase phase, - @Nonnull RigidBodyKey bodyKey) implements PhysicsFrameEvent { + @Nonnull UUID bodyUuid) implements PhysicsFrameEvent { public PhysicsBodyActivationEvent { Objects.requireNonNull(spaceId, "spaceId"); Objects.requireNonNull(phase, "phase"); - Objects.requireNonNull(bodyKey, "bodyKey"); + Objects.requireNonNull(bodyUuid, "bodyUuid"); } @Nonnull diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsCommandBatchEvent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsCommandBatchEvent.java deleted file mode 100644 index 2a7ffd61..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsCommandBatchEvent.java +++ /dev/null @@ -1,106 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.events; - -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import javax.annotation.Nullable; - -/** - * Value-only owner-lane outcome for one submitted physics command batch. - * - * @param commandBatchSequence owner FIFO sequence assigned when the command batch was submitted - * @param submittedServerTick server tick supplied by the caller when the batch was recorded - * @param ownerCompletedNanoTime monotonic nano time sampled when the owner-lane command-batch - * outcome event was published, or {@code 0} when unavailable - * @param commandCount number of recorded command operations in the batch - * @param bodyKeyReferenceCount number of body-key references copied from recorded operations - * @param firstBodyKey first copied body key seen in recorded order, if any - * @param jointKeyReferenceCount number of joint-key references copied from recorded operations - * @param firstJointKey first copied joint key seen in recorded order, if any - * @param allApplied whether every command operation in the batch applied successfully - * @param firstRejectedCommandSequence one-based command sequence for the first rejection, or - * {@code 0} when no rejection is known - * @param firstRejectedMessage copied rejection reason for the first rejection, if any - */ -public record PhysicsCommandBatchEvent(long commandBatchSequence, - long submittedServerTick, - long ownerCompletedNanoTime, - int commandCount, - int bodyKeyReferenceCount, - @Nullable RigidBodyKey firstBodyKey, - int jointKeyReferenceCount, - @Nullable JointKey firstJointKey, - boolean allApplied, - long firstRejectedCommandSequence, - @Nullable String firstRejectedMessage) { - - public PhysicsCommandBatchEvent(long commandBatchSequence, - long submittedServerTick, - long ownerCompletedNanoTime, - int commandCount, - boolean allApplied, - long firstRejectedCommandSequence, - @Nullable String firstRejectedMessage) { - this(commandBatchSequence, - submittedServerTick, - ownerCompletedNanoTime, - commandCount, - 0, - null, - 0, - null, - allApplied, - firstRejectedCommandSequence, - firstRejectedMessage); - } - - public PhysicsCommandBatchEvent(long commandBatchSequence, - long submittedServerTick, - int commandCount, - boolean allApplied, - long firstRejectedCommandSequence, - @Nullable String firstRejectedMessage) { - this(commandBatchSequence, - submittedServerTick, - 0L, - commandCount, - 0, - null, - 0, - null, - allApplied, - firstRejectedCommandSequence, - firstRejectedMessage); - } - - public PhysicsCommandBatchEvent { - commandBatchSequence = Math.max(0L, commandBatchSequence); - submittedServerTick = Math.max(0L, submittedServerTick); - ownerCompletedNanoTime = Math.max(0L, ownerCompletedNanoTime); - commandCount = Math.max(0, commandCount); - bodyKeyReferenceCount = Math.max(0, bodyKeyReferenceCount); - if (bodyKeyReferenceCount == 0) { - firstBodyKey = null; - } - jointKeyReferenceCount = Math.max(0, jointKeyReferenceCount); - if (jointKeyReferenceCount == 0) { - firstJointKey = null; - } - firstRejectedCommandSequence = Math.max(0L, firstRejectedCommandSequence); - if (allApplied) { - firstRejectedCommandSequence = 0L; - firstRejectedMessage = null; - } - } - - public boolean hasOwnerCompletionTimestamp() { - return ownerCompletedNanoTime > 0L; - } - - public boolean referencesBodies() { - return bodyKeyReferenceCount > 0; - } - - public boolean referencesJoints() { - return jointKeyReferenceCount > 0; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsContactEvent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsContactEvent.java index bedd1899..e50a751d 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsContactEvent.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsContactEvent.java @@ -2,18 +2,21 @@ import dev.hytalemodding.impulse.api.PhysicsContactPhase; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; import java.util.Objects; +import java.util.UUID; import javax.annotation.Nonnull; import org.joml.Vector3f; /** * Stable contact event copied from a backend event batch. + * + * @deprecated Physics event plugin API is deprecated without replacement. */ +@Deprecated(since = "0.1.0", forRemoval = false) public record PhysicsContactEvent(@Nonnull SpaceId spaceId, @Nonnull PhysicsContactPhase phase, - @Nonnull RigidBodyKey bodyAKey, - @Nonnull RigidBodyKey bodyBKey, + @Nonnull UUID bodyAUuid, + @Nonnull UUID bodyBUuid, @Nonnull Vector3f pointOnA, @Nonnull Vector3f pointOnB, @Nonnull Vector3f normalOnB, @@ -23,8 +26,8 @@ public record PhysicsContactEvent(@Nonnull SpaceId spaceId, public PhysicsContactEvent { Objects.requireNonNull(spaceId, "spaceId"); Objects.requireNonNull(phase, "phase"); - Objects.requireNonNull(bodyAKey, "bodyAKey"); - Objects.requireNonNull(bodyBKey, "bodyBKey"); + Objects.requireNonNull(bodyAUuid, "bodyAUuid"); + Objects.requireNonNull(bodyBUuid, "bodyBUuid"); pointOnA = new Vector3f(Objects.requireNonNull(pointOnA, "pointOnA")); pointOnB = new Vector3f(Objects.requireNonNull(pointOnB, "pointOnB")); normalOnB = new Vector3f(Objects.requireNonNull(normalOnB, "normalOnB")); diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsEventCollectionMode.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsEventCollectionMode.java similarity index 75% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsEventCollectionMode.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsEventCollectionMode.java index eaef2a16..8ff3876f 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsEventCollectionMode.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsEventCollectionMode.java @@ -1,20 +1,25 @@ -package dev.hytalemodding.impulse.core.plugin.settings; +package dev.hytalemodding.impulse.core.plugin.events; import java.util.Locale; import javax.annotation.Nonnull; /** - * Controls which backend physics events are collected during owner-lane steps. + * Controls which backend physics events are collected during store tick steps. + * + * @deprecated Physics event plugin API is deprecated without replacement. */ +@Deprecated(since = "0.1.0", forRemoval = false) public enum PhysicsEventCollectionMode { /** * Steps physics spaces without collecting backend event batches. */ + @Deprecated(since = "0.1.0", forRemoval = false) DISABLED("disabled"), /** * Collects backend contact events and publishes stable physics event frames. */ + @Deprecated(since = "0.1.0", forRemoval = false) CONTACTS("contacts"); @Nonnull diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsEventFrame.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsEventFrame.java index 3b276e23..ec6d0d1e 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsEventFrame.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsEventFrame.java @@ -6,9 +6,9 @@ import javax.annotation.Nullable; /** - * Immutable value frame of physics-owner events. + * Immutable value frame of PhysicsStore events. * - *

Event frames describe owner-lane outcomes. They are distinct from command + *

Event frames describe store tick lane outcomes. They are distinct from command * contexts, backend handles, and published body snapshots.

* *

The runtime keeps only the latest frame. This is useful for diagnostics, latency @@ -21,21 +21,19 @@ * @param latestCapturedSnapshotStepSequence step-scheduler sequence carried by that latest captured snapshot * frame * @param latestCapturedSnapshotServerTick server tick carried by that latest captured snapshot frame - * @param latestCapturedSnapshotLastIncludedCommandBatchSequence latest command-batch sequence - * included by that captured snapshot frame - * @param commandBatches copied command-batch outcome events in this frame * @param steps copied physics step snapshot-capture events in this frame * @param snapshotPublications copied reader-side snapshot-publication events in this frame * @param physicsEvents copied stable physics events in this frame * @param droppedBackendEventCount backend events dropped while bounded buffers were full + * + * @deprecated Physics event plugin API is deprecated without replacement. */ +@Deprecated(since = "0.1.0", forRemoval = false) public record PhysicsEventFrame(long frameSequence, long worldEpoch, long latestCapturedSnapshotFrameEpoch, long latestCapturedSnapshotStepSequence, long latestCapturedSnapshotServerTick, - long latestCapturedSnapshotLastIncludedCommandBatchSequence, - @Nonnull List commandBatches, @Nonnull List steps, @Nonnull List snapshotPublications, @Nonnull List physicsEvents, @@ -47,9 +45,6 @@ public record PhysicsEventFrame(long frameSequence, latestCapturedSnapshotFrameEpoch = Math.max(0L, latestCapturedSnapshotFrameEpoch); latestCapturedSnapshotStepSequence = Math.max(0L, latestCapturedSnapshotStepSequence); latestCapturedSnapshotServerTick = Math.max(0L, latestCapturedSnapshotServerTick); - latestCapturedSnapshotLastIncludedCommandBatchSequence = - Math.max(0L, latestCapturedSnapshotLastIncludedCommandBatchSequence); - commandBatches = List.copyOf(Objects.requireNonNull(commandBatches, "commandBatches")); steps = List.copyOf(Objects.requireNonNull(steps, "steps")); snapshotPublications = List.copyOf(Objects.requireNonNull(snapshotPublications, "snapshotPublications")); @@ -60,8 +55,6 @@ public record PhysicsEventFrame(long frameSequence, public PhysicsEventFrame(long frameSequence, long worldEpoch, long latestCapturedSnapshotFrameEpoch, - long latestCapturedSnapshotLastIncludedCommandBatchSequence, - @Nonnull List commandBatches, @Nonnull List steps, @Nonnull List snapshotPublications) { this(frameSequence, @@ -69,32 +62,12 @@ public PhysicsEventFrame(long frameSequence, latestCapturedSnapshotFrameEpoch, 0L, 0L, - latestCapturedSnapshotLastIncludedCommandBatchSequence, - commandBatches, steps, snapshotPublications, List.of(), 0); } - public PhysicsEventFrame(long frameSequence, - long worldEpoch, - long latestCapturedSnapshotFrameEpoch, - long latestCapturedSnapshotLastIncludedCommandBatchSequence, - @Nonnull List commandBatches) { - this(frameSequence, - worldEpoch, - latestCapturedSnapshotFrameEpoch, - 0L, - 0L, - latestCapturedSnapshotLastIncludedCommandBatchSequence, - commandBatches, - List.of(), - List.of(), - List.of(), - 0); - } - @Nonnull public static PhysicsEventFrame empty(long worldEpoch) { return new PhysicsEventFrame(0L, @@ -102,18 +75,12 @@ public static PhysicsEventFrame empty(long worldEpoch) { 0L, 0L, 0L, - 0L, - List.of(), List.of(), List.of(), List.of(), 0); } - public int commandBatchCount() { - return commandBatches.size(); - } - public int stepCount() { return steps.size(); } @@ -127,19 +94,13 @@ public int physicsEventCount() { } public int eventCount() { - return commandBatchCount() + stepCount() + snapshotPublicationCount() + physicsEventCount(); + return stepCount() + snapshotPublicationCount() + physicsEventCount(); } public boolean isEmpty() { return eventCount() == 0; } - @Nullable - public PhysicsCommandBatchEvent latestCommandBatch() { - int count = commandBatches.size(); - return count == 0 ? null : commandBatches.get(count - 1); - } - @Nullable public PhysicsStepEvent latestStep() { int count = steps.size(); @@ -152,23 +113,6 @@ public PhysicsSnapshotPublicationEvent latestSnapshotPublication() { return count == 0 ? null : snapshotPublications.get(count - 1); } - public boolean latestCapturedSnapshotIncludes(@Nonnull PhysicsCommandBatchEvent event) { - return latestCapturedSnapshotIncludesCommandBatch( - Objects.requireNonNull(event, "event").commandBatchSequence()); - } - - public boolean latestCapturedSnapshotIncludesCommandBatch(long commandBatchSequence) { - return commandBatchSequence > 0L - && latestCapturedSnapshotLastIncludedCommandBatchSequence >= commandBatchSequence; - } - - public long capturedSnapshotServerTickLatency(@Nonnull PhysicsCommandBatchEvent event) { - if (!latestCapturedSnapshotIncludes(event)) { - return 0L; - } - return latestCapturedSnapshotServerTickLatencyFromSubmittedTick(event.submittedServerTick()); - } - public long latestCapturedSnapshotServerTickLatencyFromSubmittedTick(long submittedServerTick) { long submitted = Math.max(0L, submittedServerTick); if (latestCapturedSnapshotServerTick <= 0L || latestCapturedSnapshotServerTick < submitted) { diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsEventFramePublishedEvent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsEventFramePublishedEvent.java index d4d2352c..1cc91eaf 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsEventFramePublishedEvent.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsEventFramePublishedEvent.java @@ -6,7 +6,10 @@ /** * Hytale world event published once for each Impulse physics event frame. + * + * @deprecated Physics event plugin API is deprecated without replacement. */ +@Deprecated(since = "0.1.0", forRemoval = false) public final class PhysicsEventFramePublishedEvent extends EcsEvent { @Nonnull diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsFrameEvent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsFrameEvent.java index bad60185..82f5c28b 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsFrameEvent.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsFrameEvent.java @@ -4,7 +4,10 @@ /** * Stable Impulse event copied into a physics event frame. + * + * @deprecated Physics event plugin API is deprecated without replacement. */ +@Deprecated(since = "0.1.0", forRemoval = false) public interface PhysicsFrameEvent { @Nonnull diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsFrameEventKind.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsFrameEventKind.java index 092cc24f..9ec890fa 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsFrameEventKind.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsFrameEventKind.java @@ -2,9 +2,15 @@ /** * Stable Impulse frame-event discriminator. + * + * @deprecated Physics event plugin API is deprecated without replacement. */ +@Deprecated(since = "0.1.0", forRemoval = false) public enum PhysicsFrameEventKind { + @Deprecated(since = "0.1.0", forRemoval = false) CONTACT, + @Deprecated(since = "0.1.0", forRemoval = false) BODY_ACTIVATION, + @Deprecated(since = "0.1.0", forRemoval = false) JOINT_BREAK } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsJointBreakEvent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsJointBreakEvent.java index 6c7aab1d..4caebe53 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsJointBreakEvent.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsJointBreakEvent.java @@ -1,23 +1,25 @@ package dev.hytalemodding.impulse.core.plugin.events; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; import java.util.Objects; +import java.util.UUID; import javax.annotation.Nonnull; import javax.annotation.Nullable; /** * Stable joint-break event copied from a backend event batch. + * + * @deprecated Physics event plugin API is deprecated without replacement. */ +@Deprecated(since = "0.1.0", forRemoval = false) public record PhysicsJointBreakEvent(@Nonnull SpaceId spaceId, - @Nonnull JointKey jointKey, - @Nullable RigidBodyKey bodyAKey, - @Nullable RigidBodyKey bodyBKey) implements PhysicsFrameEvent { + @Nonnull UUID jointUuid, + @Nullable UUID bodyAUuid, + @Nullable UUID bodyBUuid) implements PhysicsFrameEvent { public PhysicsJointBreakEvent { Objects.requireNonNull(spaceId, "spaceId"); - Objects.requireNonNull(jointKey, "jointKey"); + Objects.requireNonNull(jointUuid, "jointUuid"); } @Nonnull diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsSnapshotPublicationEvent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsSnapshotPublicationEvent.java index b87e7a79..83c7fc82 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsSnapshotPublicationEvent.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsSnapshotPublicationEvent.java @@ -1,7 +1,5 @@ package dev.hytalemodding.impulse.core.plugin.events; -import javax.annotation.Nonnull; - /** * Value-only event for applying a published snapshot frame to reader-side stores. * @@ -9,18 +7,19 @@ * @param worldEpoch frame world epoch * @param stepSequence Impulse step-scheduler sequence carried by the frame * @param serverTick Hytale server tick carried by the frame - * @param lastIncludedCommandBatchSequence latest completed command batch included by the frame * @param publicationServerTick Hytale server tick observed when this frame was applied to * reader-side stores, or {@code 0} when unavailable * @param publicationNanoTime monotonic nano time sampled when this publication event was created, * or {@code 0} when unavailable * @param appliedBodyCount number of body snapshots applied to reader-side stores + * + * @deprecated Physics event plugin API is deprecated without replacement. */ +@Deprecated(since = "0.1.0", forRemoval = false) public record PhysicsSnapshotPublicationEvent(long snapshotFrameEpoch, long worldEpoch, long stepSequence, long serverTick, - long lastIncludedCommandBatchSequence, long publicationServerTick, long publicationNanoTime, int appliedBodyCount) { @@ -29,13 +28,11 @@ public PhysicsSnapshotPublicationEvent(long snapshotFrameEpoch, long worldEpoch, long stepSequence, long serverTick, - long lastIncludedCommandBatchSequence, int appliedBodyCount) { this(snapshotFrameEpoch, worldEpoch, stepSequence, serverTick, - lastIncludedCommandBatchSequence, 0L, 0L, appliedBodyCount); @@ -46,40 +43,15 @@ public PhysicsSnapshotPublicationEvent(long snapshotFrameEpoch, worldEpoch = Math.max(0L, worldEpoch); stepSequence = Math.max(0L, stepSequence); serverTick = Math.max(0L, serverTick); - lastIncludedCommandBatchSequence = Math.max(0L, lastIncludedCommandBatchSequence); publicationServerTick = Math.max(0L, publicationServerTick); publicationNanoTime = Math.max(0L, publicationNanoTime); appliedBodyCount = Math.max(0, appliedBodyCount); } - public boolean includesCommandBatch(long commandBatchSequence) { - return commandBatchSequence > 0L - && lastIncludedCommandBatchSequence >= commandBatchSequence; - } - - public boolean includesCommandBatch(@Nonnull PhysicsCommandBatchEvent event) { - return includesCommandBatch(event.commandBatchSequence()); - } - public long frameToPublicationServerTickLatency() { if (publicationServerTick <= 0L || publicationServerTick < serverTick) { return 0L; } return publicationServerTick - serverTick; } - - public long commandToPublicationServerTickLatency(@Nonnull PhysicsCommandBatchEvent event) { - if (!includesCommandBatch(event)) { - return 0L; - } - return serverTickLatencyFrom(event.submittedServerTick()); - } - - private long serverTickLatencyFrom(long submittedServerTick) { - long submitted = Math.max(0L, submittedServerTick); - if (publicationServerTick <= 0L || publicationServerTick < submitted) { - return 0L; - } - return publicationServerTick - submitted; - } } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsStepEvent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsStepEvent.java index 584862ad..b2255255 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsStepEvent.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/PhysicsStepEvent.java @@ -5,22 +5,23 @@ import javax.annotation.Nonnull; /** - * Value-only event for a physics owner step that captured a snapshot frame. + * Value-only event for a store tick step that captured a snapshot frame. * * @param stepSequence Impulse step-scheduler sequence copied into the captured snapshot * @param serverTick Hytale server tick copied into the captured snapshot * @param snapshotFrameEpoch captured snapshot frame epoch * @param snapshotStatus captured snapshot status - * @param lastIncludedCommandBatchSequence latest completed command batch included by the capture * @param bodyCount number of body snapshots captured in the frame * @param stepNanos profiled step duration, or {@code 0} when profiling was disabled * @param snapshotNanos profiled snapshot capture duration, or {@code 0} when profiling was disabled + * + * @deprecated Physics event plugin API is deprecated without replacement. */ +@Deprecated(since = "0.1.0", forRemoval = false) public record PhysicsStepEvent(long stepSequence, long serverTick, long snapshotFrameEpoch, @Nonnull PublishedPhysicsSnapshotFrame.Status snapshotStatus, - long lastIncludedCommandBatchSequence, int bodyCount, long stepNanos, long snapshotNanos) { @@ -30,7 +31,6 @@ public record PhysicsStepEvent(long stepSequence, serverTick = Math.max(0L, serverTick); snapshotFrameEpoch = Math.max(0L, snapshotFrameEpoch); Objects.requireNonNull(snapshotStatus, "snapshotStatus"); - lastIncludedCommandBatchSequence = Math.max(0L, lastIncludedCommandBatchSequence); bodyCount = Math.max(0, bodyCount); stepNanos = Math.max(0L, stepNanos); snapshotNanos = Math.max(0L, snapshotNanos); diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/package-info.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/package-info.java new file mode 100644 index 00000000..7fd0efbe --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/events/package-info.java @@ -0,0 +1,7 @@ +/** + * Public physics event values and publication hooks. + * + * @deprecated Physics event plugin API is deprecated without replacement. + */ +@Deprecated(since = "0.1.0", forRemoval = false) +package dev.hytalemodding.impulse.core.plugin.events; diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/joint/JointKey.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/joint/JointKey.java deleted file mode 100644 index 35042956..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/joint/JointKey.java +++ /dev/null @@ -1,82 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.joint; - -import dev.hytalemodding.impulse.core.internal.math.UuidMath; -import java.util.Objects; -import java.util.concurrent.ThreadLocalRandom; -import java.util.UUID; -import javax.annotation.Nonnull; - -/** - * Stable Impulse-side identity for a physics joint. - * - *

Backend {@code PhysicsJoint} handles are live owner-lane objects. This id is the handle - * component state and plugin-facing lifecycle code should retain.

- */ -public final class JointKey { - - private final long mostSignificantBits; - private final long leastSignificantBits; - - public JointKey(@Nonnull UUID value) { - this(Objects.requireNonNull(value, "value").getMostSignificantBits(), - value.getLeastSignificantBits()); - } - - private JointKey(long mostSignificantBits, - long leastSignificantBits) { - this.mostSignificantBits = mostSignificantBits; - this.leastSignificantBits = leastSignificantBits; - } - - @Nonnull - public static JointKey random() { - ThreadLocalRandom random = ThreadLocalRandom.current(); - long mostSignificantBits = UuidMath.version4MostSignificantBits(random.nextLong()); - long leastSignificantBits = UuidMath.ietfVariantLeastSignificantBits(random.nextLong()); - return new JointKey(mostSignificantBits, leastSignificantBits); - } - - @Nonnull - public static JointKey of(@Nonnull UUID value) { - return new JointKey(value); - } - - @Nonnull - public static JointKey of(long mostSignificantBits, - long leastSignificantBits) { - return new JointKey(mostSignificantBits, leastSignificantBits); - } - - @Nonnull - public UUID value() { - return new UUID(mostSignificantBits, leastSignificantBits); - } - - public long mostSignificantBits() { - return mostSignificantBits; - } - - public long leastSignificantBits() { - return leastSignificantBits; - } - - @Override - public boolean equals(Object other) { - return this == other - || other instanceof JointKey key - && mostSignificantBits == key.mostSignificantBits - && leastSignificantBits == key.leastSignificantBits; - } - - @Override - public int hashCode() { - long bits = mostSignificantBits ^ leastSignificantBits; - return (int) (bits >> Integer.SIZE) ^ (int) bits; - } - - @Nonnull - @Override - public String toString() { - return value().toString(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/control/ImpulseControlPlugin.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/control/ImpulseControlPlugin.java deleted file mode 100644 index 638e1c8c..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/control/ImpulseControlPlugin.java +++ /dev/null @@ -1,46 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.modules.control; - -import com.hypixel.hytale.component.ComponentRegistryProxy; -import com.hypixel.hytale.server.core.plugin.JavaPlugin; -import com.hypixel.hytale.server.core.plugin.JavaPluginInit; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.modules.control.ControlLifecycle; -import dev.hytalemodding.impulse.core.internal.modules.control.components.PhysicsControlSessionComponent; -import dev.hytalemodding.impulse.core.internal.modules.control.systems.PhysicsControllableLifecycleSystem; -import dev.hytalemodding.impulse.core.internal.modules.control.systems.PhysicsControlRuntimeHolderSystem; -import dev.hytalemodding.impulse.core.internal.modules.control.systems.PhysicsControlSessionCleanupSystem; -import dev.hytalemodding.impulse.core.internal.modules.control.systems.PhysicsKinematicControlSystem; -import javax.annotation.Nonnull; - -/** - * Subplugin that enables Impulse kinematic control sessions. - */ -public final class ImpulseControlPlugin extends JavaPlugin { - - public ImpulseControlPlugin(@Nonnull JavaPluginInit init) { - super(init); - } - - @Override - protected void setup() { - ComponentRegistryProxy entityRegistry = getEntityStoreRegistry(); - ImpulseControllableComponent.setComponentType(entityRegistry.registerComponent( - ImpulseControllableComponent.class, - "ImpulseControllable", - ImpulseControllableComponent.CODEC)); - PhysicsControlSessionComponent.setComponentType(entityRegistry.registerComponent( - PhysicsControlSessionComponent.class, PhysicsControlSessionComponent::new)); - entityRegistry.registerSystem(new PhysicsControlRuntimeHolderSystem()); - entityRegistry.registerSystem(new PhysicsControllableLifecycleSystem()); - entityRegistry.registerSystem(new PhysicsControlSessionCleanupSystem()); - entityRegistry.registerSystem(new PhysicsKinematicControlSystem()); - ControlLifecycle.enable(); - } - - @Override - protected void shutdown() { - ControlLifecycle.disable(); - ImpulseControllableComponent.clearComponentType(); - PhysicsControlSessionComponent.clearComponentType(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/control/PhysicsControlSessions.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/control/PhysicsControlSessions.java deleted file mode 100644 index 7270a573..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/control/PhysicsControlSessions.java +++ /dev/null @@ -1,214 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.modules.control; - -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.modules.control.ControlLifecycle; -import dev.hytalemodding.impulse.core.internal.modules.control.components.PhysicsControlSessionComponent; -import dev.hytalemodding.impulse.core.internal.modules.control.systems.PhysicsKinematicControlSystem; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.PhysicsCommandRecorder; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3f; - -/** - * Public stateless helper for Impulse-managed kinematic control sessions. - */ -public final class PhysicsControlSessions { - - private PhysicsControlSessions() { - } - - /** - * Returns whether the control subplugin is loaded and its component types are registered. - */ - public static boolean isAvailable() { - return ControlLifecycle.isEnabled() - && ImpulseControllableComponent.isComponentTypeRegistered() - && PhysicsControlSessionComponent.isComponentTypeRegistered(); - } - - /** - * Returns whether the controller entity currently has an active Impulse control session. - */ - public static boolean hasSession(@Nonnull Store store, - @Nonnull Ref controllerRef) { - if (!isAvailable()) { - return false; - } - PhysicsControlSessionComponent session = - store.getComponent(controllerRef, PhysicsControlSessionComponent.getComponentType()); - return session != null && session.isActive(); - } - - /** - * Starts or replaces the controller entity's Impulse control session and marks the body as - * externally controlled. - */ - public static void startSession(@Nonnull Store store, - @Nonnull Ref controllerRef, - @Nonnull RigidBodyKey bodyKey, - @Nonnull RigidBodyKey anchorBodyKey, - @Nullable Ref targetRef, - @Nullable SpaceId spaceId, - @Nonnull PhysicsBodyType originalBodyType, - float grabDistance, - @Nonnull Vector3f viewOffset, - @Nonnull Vector3f previousTarget) { - startSession(store, - controllerRef, - bodyKey, - anchorBodyKey, - null, - targetRef, - spaceId, - originalBodyType, - grabDistance, - viewOffset, - previousTarget); - } - - /** - * Starts or replaces the controller entity's Impulse control session with the created control - * joint handle. - */ - public static void startSession(@Nonnull Store store, - @Nonnull Ref controllerRef, - @Nonnull RigidBodyKey bodyKey, - @Nonnull RigidBodyKey anchorBodyKey, - @Nullable JointKey controlJointKey, - @Nullable Ref targetRef, - @Nullable SpaceId spaceId, - @Nonnull PhysicsBodyType originalBodyType, - float grabDistance, - @Nonnull Vector3f viewOffset, - @Nonnull Vector3f previousTarget) { - requireAvailable(); - ControlLifecycle.registerStore(store); - PhysicsWorldRuntimeResource resource = PhysicsWorldRuntimeResource.require(store); - ComponentType sessionType = - PhysicsControlSessionComponent.getComponentType(); - releaseSession(resource, store, controllerRef, sessionType); - store.putComponent(controllerRef, - sessionType, - new PhysicsControlSessionComponent(bodyKey, - anchorBodyKey, - controlJointKey, - targetRef, - spaceId, - originalBodyType, - grabDistance, - viewOffset, - previousTarget)); - resource.markBodyControlled(bodyKey); - } - - /** - * Releases and removes an active Impulse control session. - * - * @return false when no session existed - */ - public static boolean releaseSession(@Nonnull Store store, - @Nonnull Ref controllerRef) { - if (!isAvailable()) { - return false; - } - return releaseSession(PhysicsWorldRuntimeResource.require(store), - store, - controllerRef, - PhysicsControlSessionComponent.getComponentType()); - } - - private static boolean releaseSession(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull Store store, - @Nonnull Ref controllerRef, - @Nonnull ComponentType sessionType) { - PhysicsControlSessionComponent session = - store.getComponent(controllerRef, sessionType); - if (session == null) { - return false; - } - - releaseSession(resource, store, controllerRef, sessionType, session); - return true; - } - - private static void releaseSession(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull Store store, - @Nonnull Ref controllerRef, - @Nonnull ComponentType sessionType, - @Nonnull PhysicsControlSessionComponent session) { - RigidBodyKey bodyKey = session.getBodyKey(); - RigidBodyKey anchorBodyKey = session.getAnchorBodyKey(); - JointKey controlJointKey = session.getControlJointKey(); - SpaceId spaceId = session.getSpaceId(); - PhysicsKinematicControlSystem.clearMutationState(store, anchorBodyKey); - if (bodyKey != null) { - resource.clearControlledBody(bodyKey); - } - boolean releaseJoint = (spaceId != null && anchorBodyKey != null && bodyKey != null) - || controlJointKey != null; - boolean restoreBody = bodyKey != null && resource.getBodyRegistrationView(bodyKey) != null; - if (releaseJoint || restoreBody || anchorBodyKey != null) { - resource.rejectSynchronousCompletionCallbackWait("release control session"); - /* - * Explicit release/start helpers keep synchronous semantics so command handlers can - * report replacement state immediately. Tick-driven cleanup uses - * PhysicsControlSessionCleanup and does not join the owner lane. - */ - resource.submitCommands(0L, 4, commands -> { - addJointReleaseCommand(commands, controlJointKey, spaceId, anchorBodyKey, bodyKey); - if (restoreBody) { - PhysicsBodyType originalBodyType = session.getOriginalBodyType(); - commands.setBodyType(bodyKey, originalBodyType); - if (originalBodyType == PhysicsBodyType.DYNAMIC) { - Vector3f releaseVelocity = session.getReleaseVelocity(); - commands.setBodyVelocity(bodyKey, - releaseVelocity.x, - releaseVelocity.y, - releaseVelocity.z, - 0.0f, - 0.0f, - 0.0f, - true); - } else { - commands.setBodyVelocity(bodyKey, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, true); - } - } - if (anchorBodyKey != null) { - commands.destroyBody(anchorBodyKey); - } - }).completionSummary().toCompletableFuture().join(); - } - - session.deactivate(); - store.removeComponent(controllerRef, sessionType); - } - - private static void requireAvailable() { - ControlLifecycle.requireEnabled(); - if (!ImpulseControllableComponent.isComponentTypeRegistered() - || !PhysicsControlSessionComponent.isComponentTypeRegistered()) { - throw new IllegalStateException( - "Impulse control is disabled. Enable HytaleModding:ImpulseControl to start control sessions."); - } - } - - private static void addJointReleaseCommand(@Nonnull PhysicsCommandRecorder commands, - @Nullable JointKey controlJointKey, - @Nullable SpaceId spaceId, - @Nullable RigidBodyKey anchorBodyKey, - @Nullable RigidBodyKey bodyKey) { - if (spaceId != null && anchorBodyKey != null && bodyKey != null) { - commands.destroyJointBetween(controlJointKey, spaceId, anchorBodyKey, bodyKey); - } else if (controlJointKey != null) { - commands.destroyJoint(controlJointKey); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollision.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollision.java new file mode 100644 index 00000000..d1c798b0 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollision.java @@ -0,0 +1,298 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicschunk; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkLifecycle; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkCollisionStreamingResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkStoreTypes; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionMutationQueueResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkSettingsIndexResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkSettingsIndexResource.PhysicsChunkSpaceSettings; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.ChunkCollisionSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3d; + +/** + * Public PhysicsChunk operations for chunk-backed collision. + */ +public final class PhysicsChunkCollision { + + private PhysicsChunkCollision() { + } + + public static boolean isSubPluginEnabled() { + return PhysicsChunkLifecycle.isEnabled(); + } + + public static boolean isChunkCollisionBody(@Nonnull Store store, + @Nonnull UUID bodyUuid) { + Store checkedStore = requireWorldThread(store, + "read PhysicsChunk body ownership"); + Ref bodyRef = PhysicsEntities.resolveRef(checkedStore, + Objects.requireNonNull(bodyUuid, "bodyUuid")); + return bodyRef != null && isChunkCollisionBody(checkedStore, bodyRef); + } + + public static boolean isChunkCollisionBody(@Nonnull Store store, + @Nullable Ref bodyRef) { + Store checkedStore = requireWorldThread(store, + "read PhysicsChunk body ownership"); + if (bodyRef == null || bodyRef.getStore() != checkedStore || !bodyRef.isValid()) { + return false; + } + return checkedStore.getComponent(bodyRef, ChunkCollisionSourceComponent.getComponentType()) + != null; + } + + @Nonnull + public static PhysicsChunkCollisionBuildStats rebuildAround(@Nonnull World world, + @Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull Vector3d center, + int radius) { + requireEnabled(); + Store checkedStore = requireMatchingWorldThread(world, + store, + "rebuild PhysicsChunk collision"); + return rebuildAroundChecked(world, checkedStore, spaceRef, center, radius); + } + + @Nonnull + private static PhysicsChunkCollisionBuildStats rebuildAroundChecked(@Nonnull World world, + @Nonnull Store checkedStore, + @Nonnull Ref spaceRef, + @Nonnull Vector3d center, + int radius) { + PhysicsChunkSpaceSettings settings = requireSettings(checkedStore, spaceRef); + PhysicsChunkCollisionMutationQueueResource queue = stampedQueue(checkedStore); + int removed = clearSpaceChunkCollisionRows(world, checkedStore, settings.spaceUuid()); + PhysicsChunkCollisionPrewarmStats stats = streaming(world).ensureAround(world, + settings.spaceUuid(), + queue, + List.of(Objects.requireNonNull(center, "center")), + radius, + Math.max(0L, world.getTick()), + null, + settings.buildOptions()); + return withRemovedBodies(stats.buildStats(), + stats.buildStats().removedBodies() + removed); + } + + @Nonnull + public static PhysicsChunkCollisionBuildStats refreshAround(@Nonnull World world, + @Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull Vector3d center, + int radius) { + requireEnabled(); + Store checkedStore = requireMatchingWorldThread(world, + store, + "refresh PhysicsChunk collision"); + return refreshAroundChecked(world, checkedStore, spaceRef, center, radius); + } + + @Nonnull + private static PhysicsChunkCollisionBuildStats refreshAroundChecked(@Nonnull World world, + @Nonnull Store checkedStore, + @Nonnull Ref spaceRef, + @Nonnull Vector3d center, + int radius) { + PhysicsChunkSpaceSettings settings = requireSettings(checkedStore, spaceRef); + return streaming(world).refreshAround(world, + settings.spaceUuid(), + stampedQueue(checkedStore), + Objects.requireNonNull(center, "center"), + radius, + Math.max(0L, world.getTick()), + null, + settings.buildOptions()); + } + + @Nonnull + public static PhysicsChunkCollisionPrewarmStats ensureAround(@Nonnull World world, + @Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull Iterable centers, + int radius, + long tick) { + requireEnabled(); + Store checkedStore = requireMatchingWorldThread(world, + store, + "ensure PhysicsChunk collision"); + return ensureAroundChecked(world, checkedStore, spaceRef, centers, radius, tick); + } + + @Nonnull + private static PhysicsChunkCollisionPrewarmStats ensureAroundChecked(@Nonnull World world, + @Nonnull Store checkedStore, + @Nonnull Ref spaceRef, + @Nonnull Iterable centers, + int radius, + long tick) { + PhysicsChunkSpaceSettings settings = requireSettings(checkedStore, spaceRef); + return streaming(world).ensureAround(world, + settings.spaceUuid(), + stampedQueue(checkedStore), + Objects.requireNonNull(centers, "centers"), + radius, + tick, + null, + settings.buildOptions()); + } + + public static int clearSpace(@Nonnull World world, + @Nonnull Store store, + @Nonnull Ref spaceRef) { + Store checkedStore = requireMatchingWorldThread(world, + store, + "clear PhysicsChunk collision"); + return clearSpaceChunkCollisionRows(world, checkedStore, requireSpaceUuid(checkedStore, + spaceRef)); + } + + @Nonnull + public static PhysicsChunkCollisionStats stats(@Nonnull World world) { + Objects.requireNonNull(world, "world"); + if (!world.isInThread()) { + throw new IllegalStateException("Cannot read PhysicsChunk collision stats " + + "outside the owning world thread"); + } + return isSubPluginEnabled() + ? streaming(world).stats() + : new PhysicsChunkCollisionStats(0, 0, 0, 0); + } + + private static void requireEnabled() { + if (!isSubPluginEnabled()) { + throw new IllegalStateException("Impulse physics chunk subplugin is disabled"); + } + } + + @Nonnull + private static Store requireWorldThread(@Nonnull Store store, + @Nonnull String operation) { + Store checkedStore = Objects.requireNonNull(store, "store"); + PhysicsThreading.requireWorldThread(checkedStore, operation); + return checkedStore; + } + + @Nonnull + private static Store requireMatchingWorldThread(@Nonnull World world, + @Nonnull Store store, + @Nonnull String operation) { + World checkedWorld = Objects.requireNonNull(world, "world"); + Store checkedStore = requireWorldThread(store, operation); + if (PhysicsThreading.world(checkedStore) != checkedWorld) { + throw new IllegalArgumentException("PhysicsStore does not belong to the supplied world"); + } + return checkedStore; + } + + @Nonnull + private static PhysicsChunkSpaceSettings requireSettings(@Nonnull Store store, + @Nonnull Ref spaceRef) { + Ref checkedRef = requireValidSpaceRef(store, spaceRef); + UUID spaceUuid = requireSpaceUuid(store, checkedRef); + ChunkCollisionSettingsComponent component = + store.getComponent(checkedRef, ChunkCollisionSettingsComponent.getComponentType()); + ChunkCollisionSettingsComponent settings = + component != null ? component : new ChunkCollisionSettingsComponent(); + if (settings.getMode() == PhysicsChunkCollisionMode.NONE) { + throw new IllegalStateException("PhysicsChunk collision is disabled for space " + + spaceUuid); + } + return new PhysicsChunkSpaceSettings(spaceUuid, + settings.getMode(), + settings.getEntityChunkBoundaryMode(), + settings.isNativeVoxelCollisionEnabled(), + settings.getRadius(), + settings.getBodyRadius(), + settings.getTtlTicks()); + } + + @Nonnull + private static Ref requireValidSpaceRef(@Nonnull Store store, + @Nonnull Ref spaceRef) { + Ref checkedRef = Objects.requireNonNull(spaceRef, "spaceRef"); + if (checkedRef.getStore() != store || !checkedRef.isValid()) { + throw new IllegalArgumentException("PhysicsStore space entity is not valid: " + + checkedRef); + } + if (store.getComponent(checkedRef, SpaceComponent.getComponentType()) == null) { + throw new IllegalArgumentException("PhysicsStore entity is not a space entity: " + + checkedRef); + } + return checkedRef; + } + + @Nonnull + private static UUID requireSpaceUuid(@Nonnull Store store, + @Nonnull Ref spaceRef) { + Ref checkedRef = requireValidSpaceRef(store, spaceRef); + UuidComponent uuid = store.getComponent(checkedRef, UuidComponent.getComponentType()); + if (uuid == null) { + throw new IllegalStateException("PhysicsStore space entity has no UUID: " + + checkedRef); + } + return uuid.getUuid(); + } + + @Nonnull + private static PhysicsChunkCollisionStreamingResource streaming(@Nonnull World world) { + Store entityStore = Objects.requireNonNull(world, "world") + .getEntityStore() + .getStore(); + return entityStore.getResource(PhysicsChunkCollisionStreamingResource.getResourceType()); + } + + private static int clearSpaceChunkCollisionRows(@Nonnull World world, + @Nonnull Store store, + @Nonnull UUID spaceUuid) { + int removed = 0; + if (isSubPluginEnabled()) { + removed = streaming(world).clearSpace(spaceUuid, stampedQueue(store)); + } + int directlyRemoved = + PhysicsChunkStoreTypes.clearChunkCollisionRowsForSpace(store, spaceUuid); + return removed != 0 ? removed : directlyRemoved; + } + + @Nonnull + private static PhysicsChunkCollisionMutationQueueResource stampedQueue( + @Nonnull Store store) { + PhysicsChunkCollisionMutationQueueResource queue = store.getResource( + PhysicsChunkCollisionMutationQueueResource.getResourceType()); + PhysicsChunkSettingsIndexResource settingsIndex = store.getResource( + PhysicsChunkSettingsIndexResource.getResourceType()); + queue.updateStamp(PhysicsChunkLifecycle.generation(), settingsIndex.generation()); + return queue; + } + + @Nonnull + private static PhysicsChunkCollisionBuildStats withRemovedBodies( + @Nonnull PhysicsChunkCollisionBuildStats stats, + int removedBodies) { + return new PhysicsChunkCollisionBuildStats(stats.scannedBlocks(), + stats.solidBlocks(), + stats.culledInteriorBlocks(), + stats.fullCubeRuns(), + stats.detailBoxes(), + stats.colliderBodies(), + removedBodies, + stats.sectionsBuilt(), + stats.sectionsRebuilt(), + stats.voxelBodies()); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollisionBuildStats.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollisionBuildStats.java new file mode 100644 index 00000000..adb34ffa --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollisionBuildStats.java @@ -0,0 +1,16 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicschunk; + +/** + * Aggregate statistics from building or rebuilding streamed PhysicsChunk collision geometry. + */ +public record PhysicsChunkCollisionBuildStats(int scannedBlocks, + int solidBlocks, + int culledInteriorBlocks, + int fullCubeRuns, + int detailBoxes, + int colliderBodies, + int removedBodies, + int sectionsBuilt, + int sectionsRebuilt, + int voxelBodies) { +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollisionMode.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollisionMode.java new file mode 100644 index 00000000..5e88d811 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollisionMode.java @@ -0,0 +1,21 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicschunk; + +/** + * Controls PhysicsChunk collision body generation for a PhysicsStore space. + */ +public enum PhysicsChunkCollisionMode { + /** + * Chunk-collision bodies are disabled. + */ + NONE, + + /** + * Chunk-collision bodies are only built when explicitly requested. + */ + MANUAL, + + /** + * Chunk-collision bodies stream around players and configured physics bodies. + */ + STREAMING +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollisionPrewarmStats.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollisionPrewarmStats.java new file mode 100644 index 00000000..fbdfb15d --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollisionPrewarmStats.java @@ -0,0 +1,10 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicschunk; + +import javax.annotation.Nonnull; + +/** + * Statistics from ensuring PhysicsChunk collision around multiple target positions. + */ +public record PhysicsChunkCollisionPrewarmStats(int sectionTargets, + @Nonnull PhysicsChunkCollisionBuildStats buildStats) { +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollisionProfiling.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollisionProfiling.java new file mode 100644 index 00000000..7fd32f51 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollisionProfiling.java @@ -0,0 +1,355 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicschunk; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.profiling.PhysicsChunkProfilingResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsProfilingResource; +import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import java.util.List; +import java.util.Locale; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Public PhysicsChunk profiling helpers for command and diagnostics surfaces. + */ +public final class PhysicsChunkCollisionProfiling { + + private PhysicsChunkCollisionProfiling() { + } + + public static boolean isRuntimeProfilingEnabled(@Nonnull Store store) { + PhysicsRuntimeProfilingResource runtimeProfiling = runtimeProfiling(store); + PhysicsChunkProfilingResource collisionProfiling = collisionProfiling(store); + return runtimeProfiling.isEnabled() && collisionProfiling.isEnabled(); + } + + public static void setRuntimeProfilingEnabled(@Nonnull World world, + @Nonnull Store store, + boolean enabled) { + runtimeProfiling(store).setEnabled(enabled); + collisionProfiling(store).setEnabled(enabled); + Store physicsStore = physicsStoreOrNull(world); + if (physicsStore != null) { + physicsStore.getResource(PhysicsProfilingResource.getResourceType()) + .setEnabled(enabled); + } + } + + public static void resetRuntimeProfiling(@Nonnull World world, + @Nonnull Store store) { + runtimeProfiling(store).reset(); + collisionProfiling(store).reset(); + Store physicsStore = physicsStoreOrNull(world); + if (physicsStore != null) { + physicsStore.getResource(PhysicsProfilingResource.getResourceType()).reset(); + } + } + + @Nonnull + public static Snapshots snapshots(@Nonnull Store store) { + PhysicsChunkProfilingResource profiling = collisionProfiling(store); + return new Snapshots(profiling.getCumulativeSnapshot(), + profiling.getLatestTickSnapshot(), + profiling.getWorstTickSnapshot(), + profiling.isEnabled()); + } + + @Nonnull + public static List missingSectionSamples( + @Nonnull SnapshotView snapshot) { + return snapshot.snapshot.getMissingSectionSamples() + .stream() + .map(PhysicsChunkCollisionProfiling::view) + .toList(); + } + + @Nonnull + private static MissingSectionSampleView view( + @Nonnull PhysicsChunkProfilingResource.MissingSectionSample sample) { + PhysicsChunkProfilingResource.StreamingTargetDiagnostic target = sample.target(); + return new MissingSectionSampleView(sample.chunkX(), + sample.sectionY(), + sample.chunkZ(), + sample.reason().name().toLowerCase(Locale.ROOT), + sample.retainedEnvelopeStatus().name().toLowerCase(Locale.ROOT), + target.targetType().name().toLowerCase(Locale.ROOT), + target.bodyUuid(), + target.snapshotPosition() != null ? target.snapshotPosition().compact() : null, + target.livePosition() != null ? target.livePosition().compact() : null); + } + + @Nonnull + private static PhysicsRuntimeProfilingResource runtimeProfiling( + @Nonnull Store store) { + assert PhysicsRuntimeProfilingResource.getResourceType() != null; + return store.getResource(PhysicsRuntimeProfilingResource.getResourceType()); + } + + @Nonnull + private static PhysicsChunkProfilingResource collisionProfiling( + @Nonnull Store store) { + return store.getResource(PhysicsChunkProfilingResource.getResourceType()); + } + + @Nullable + private static Store physicsStoreOrNull(@Nonnull World world) { + return PhysicsThreading.storeOrNull(world); + } + + public record Snapshots(@Nonnull SnapshotView cumulative, + @Nonnull SnapshotView latest, + @Nonnull SnapshotView worst, + boolean enabled) { + + private Snapshots(@Nonnull PhysicsChunkProfilingResource.Snapshot cumulative, + @Nonnull PhysicsChunkProfilingResource.Snapshot latest, + @Nonnull PhysicsChunkProfilingResource.Snapshot worst, + boolean enabled) { + this(new SnapshotView(cumulative), new SnapshotView(latest), new SnapshotView(worst), + enabled); + } + } + + public static class SnapshotView { + + @Nonnull + private final PhysicsChunkProfilingResource.Snapshot snapshot; + + SnapshotView(@Nonnull PhysicsChunkProfilingResource.Snapshot snapshot) { + this.snapshot = snapshot; + } + + @Nonnull + final PhysicsChunkProfilingResource.Snapshot rawSnapshot() { + return snapshot; + } + + public int getTickSamples() { + return snapshot.getTickSamples(); + } + + public int getPlayerStreamingTargets() { + return snapshot.getPlayerStreamingTargets(); + } + + public int getBodyStreamingCandidates() { + return snapshot.getBodyStreamingCandidates(); + } + + public int getBodySpatialIndexCandidates() { + return snapshot.getBodySpatialIndexCandidates(); + } + + public int getBodyStreamingTargets() { + return snapshot.getBodyStreamingTargets(); + } + + public int getBodyTargetDedupeSkips() { + return snapshot.getBodyTargetDedupeSkips(); + } + + public int getBodyTargetCacheHits() { + return snapshot.getBodyTargetCacheHits(); + } + + public int getBodyTargetFirstSeen() { + return snapshot.getBodyTargetFirstSeen(); + } + + public int getBodyTargetBoundsChanged() { + return snapshot.getBodyTargetBoundsChanged(); + } + + public int getBodyTargetActiveRefreshes() { + return snapshot.getBodyTargetActiveRefreshes(); + } + + public int getBodyTargetSleepingRefreshes() { + return snapshot.getBodyTargetSleepingRefreshes(); + } + + public int getBodyTargetActiveStableSkips() { + return snapshot.getBodyTargetActiveStableSkips(); + } + + public int getBodyTargetSleepingStableSkips() { + return snapshot.getBodyTargetSleepingStableSkips(); + } + + public int getBodyTargetsPruned() { + return snapshot.getBodyTargetsPruned(); + } + + public int getPlayerSectionTargets() { + return snapshot.getPlayerSectionTargets(); + } + + public int getBodySectionTargets() { + return snapshot.getBodySectionTargets(); + } + + public int getStreamingSpaces() { + return snapshot.getStreamingSpaces(); + } + + public int getCollisionApplyQueued() { + return snapshot.getCollisionApplyQueued(); + } + + public int getCollisionApplySkippedPending() { + return snapshot.getCollisionApplySkippedPending(); + } + + public int getEnsureCalls() { + return snapshot.getEnsureCalls(); + } + + public int getSectionRequests() { + return snapshot.getSectionRequests(); + } + + public int getSectionCacheHits() { + return snapshot.getSectionCacheHits(); + } + + public int getMissingChunks() { + return snapshot.getMissingChunks(); + } + + public int getMissingBlockChunks() { + return snapshot.getMissingBlockChunks(); + } + + public int getMissingBlockSections() { + return snapshot.getMissingBlockSections(); + } + + public int getMissingReasonUnknown() { + return snapshot.getMissingReasonUnknown(); + } + + public int getMissingBackoffSkips() { + return snapshot.getMissingBackoffSkips(); + } + + public int getMissingBlockChunkBackoffSkips() { + return snapshot.getMissingBlockChunkBackoffSkips(); + } + + public int getMissingBlockSectionBackoffSkips() { + return snapshot.getMissingBlockSectionBackoffSkips(); + } + + public int getMissingInsideRetainedEnvelope() { + return snapshot.getMissingInsideRetainedEnvelope(); + } + + public int getMissingOutsideRetainedEnvelope() { + return snapshot.getMissingOutsideRetainedEnvelope(); + } + + public int getMissingUnconfiguredRetainedEnvelope() { + return snapshot.getMissingUnconfiguredRetainedEnvelope(); + } + + public int getSectionsBuilt() { + return snapshot.getSectionsBuilt(); + } + + public int getSectionsRebuilt() { + return snapshot.getSectionsRebuilt(); + } + + public int getVoxelBodies() { + return snapshot.getVoxelBodies(); + } + + public int getColliderBodiesAdded() { + return snapshot.getColliderBodiesAdded(); + } + + public int getBodiesRemovedFromRebuild() { + return snapshot.getBodiesRemovedFromRebuild(); + } + + public int getBodiesRemovedFromUnloadedPrune() { + return snapshot.getBodiesRemovedFromUnloadedPrune(); + } + + public int getBodiesRemovedFromTtlPrune() { + return snapshot.getBodiesRemovedFromTtlPrune(); + } + + public int getSectionsRemovedFromUnloadedPrune() { + return snapshot.getSectionsRemovedFromUnloadedPrune(); + } + + public int getSectionsRemovedFromTtlPrune() { + return snapshot.getSectionsRemovedFromTtlPrune(); + } + + public int getDuplicateSkips() { + return snapshot.getDuplicateSkips(); + } + + public int getScannedBlocks() { + return snapshot.getScannedBlocks(); + } + + public int getSolidBlocks() { + return snapshot.getSolidBlocks(); + } + + public int getCulledInteriorBlocks() { + return snapshot.getCulledInteriorBlocks(); + } + + public int getFullCubeRuns() { + return snapshot.getFullCubeRuns(); + } + + public int getDetailBoxes() { + return snapshot.getDetailBoxes(); + } + + public int getUniqueMissingSections() { + return snapshot.getUniqueMissingSections(); + } + + public long getTickNanos() { + return snapshot.getTickNanos(); + } + + public long getEnsureAroundNanos() { + return snapshot.getEnsureAroundNanos(); + } + + public long getEnsureSectionNanos() { + return snapshot.getEnsureSectionNanos(); + } + + public long getPruneUnloadedNanos() { + return snapshot.getPruneUnloadedNanos(); + } + + public long getPruneUnusedNanos() { + return snapshot.getPruneUnusedNanos(); + } + } + + public record MissingSectionSampleView(int chunkX, + int sectionY, + int chunkZ, + @Nonnull String reason, + @Nonnull String retainedEnvelopeStatus, + @Nonnull String targetType, + @Nullable UUID bodyUuid, + @Nullable String snapshotPosition, + @Nullable String livePosition) { + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollisionStats.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollisionStats.java new file mode 100644 index 00000000..741f395d --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/PhysicsChunkCollisionStats.java @@ -0,0 +1,10 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicschunk; + +/** + * Current size of the generated PhysicsChunk collision cache. + */ +public record PhysicsChunkCollisionStats(int spaces, + int sections, + int bodies, + int shapeTemplates) { +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/components/ChunkCollisionSettingsComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/components/ChunkCollisionSettingsComponent.java new file mode 100644 index 00000000..a19da58f --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/components/ChunkCollisionSettingsComponent.java @@ -0,0 +1,202 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.codec.codecs.EnumCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.components.PhysicsComponentTypes; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.EntityChunkBoundaryMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsChunkCollisionSettings; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * Authored PhysicsChunk collision streaming settings for one PhysicsStore space entity. + */ +public class ChunkCollisionSettingsComponent implements Component { + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + ChunkCollisionSettingsComponent.class, + ChunkCollisionSettingsComponent::new) + .append(new KeyedCodec<>("Mode", new EnumCodec<>(PhysicsChunkCollisionMode.class), false), + (component, value) -> component.mode = value != null + ? value + : PhysicsChunkCollisionMode.NONE, + ChunkCollisionSettingsComponent::getMode) + .add() + .append(new KeyedCodec<>("NativeVoxelCollision", Codec.BOOLEAN, false), + (component, value) -> component.nativeVoxelCollisionEnabled = value != null && value, + ChunkCollisionSettingsComponent::isNativeVoxelCollisionEnabled) + .add() + .append(new KeyedCodec<>("EntityChunkBoundaryMode", + new EnumCodec<>(EntityChunkBoundaryMode.class), + false), + (component, value) -> component.entityChunkBoundaryMode = value != null + ? value + : PhysicsChunkCollisionSettings.DEFAULT_ENTITY_CHUNK_BOUNDARY_MODE, + ChunkCollisionSettingsComponent::getEntityChunkBoundaryMode) + .add() + .append(new KeyedCodec<>("Radius", Codec.INTEGER, false), + (component, value) -> component.radius = value != null + ? value + : PhysicsChunkCollisionSettings.DEFAULT_RADIUS, + ChunkCollisionSettingsComponent::getRadius) + .add() + .append(new KeyedCodec<>("BodyRadius", Codec.INTEGER, false), + (component, value) -> component.bodyRadius = value != null + ? value + : PhysicsChunkCollisionSettings.DEFAULT_BODY_RADIUS, + ChunkCollisionSettingsComponent::getBodyRadius) + .add() + .append(new KeyedCodec<>("TtlTicks", Codec.INTEGER, false), + (component, value) -> component.ttlTicks = value != null + ? value + : PhysicsChunkCollisionSettings.DEFAULT_TTL_TICKS, + ChunkCollisionSettingsComponent::getTtlTicks) + .add() + .build(); + + @Nonnull + private PhysicsChunkCollisionMode mode = PhysicsChunkCollisionMode.NONE; + @Nonnull + private EntityChunkBoundaryMode entityChunkBoundaryMode = + PhysicsChunkCollisionSettings.DEFAULT_ENTITY_CHUNK_BOUNDARY_MODE; + private boolean nativeVoxelCollisionEnabled = + PhysicsChunkCollisionSettings.DEFAULT_NATIVE_VOXEL_COLLISION_ENABLED; + private int radius = PhysicsChunkCollisionSettings.DEFAULT_RADIUS; + private int bodyRadius = PhysicsChunkCollisionSettings.DEFAULT_BODY_RADIUS; + private int ttlTicks = PhysicsChunkCollisionSettings.DEFAULT_TTL_TICKS; + + public ChunkCollisionSettingsComponent() { + } + + public ChunkCollisionSettingsComponent(@Nonnull PhysicsChunkCollisionSettings settings) { + this(settings.getMode(), + settings.getEntityChunkBoundaryMode(), + settings.isNativeVoxelCollisionEnabled(), + settings.getRadius(), + settings.getBodyRadius(), + settings.getTtlTicks()); + } + + public ChunkCollisionSettingsComponent(@Nonnull PhysicsChunkCollisionMode mode, + boolean nativeVoxelCollisionEnabled, + int radius, + int bodyRadius, + int ttlTicks) { + this(mode, + PhysicsChunkCollisionSettings.DEFAULT_ENTITY_CHUNK_BOUNDARY_MODE, + nativeVoxelCollisionEnabled, + radius, + bodyRadius, + ttlTicks); + } + + public ChunkCollisionSettingsComponent(@Nonnull PhysicsChunkCollisionMode mode, + @Nonnull EntityChunkBoundaryMode entityChunkBoundaryMode, + boolean nativeVoxelCollisionEnabled, + int radius, + int bodyRadius, + int ttlTicks) { + this.mode = Objects.requireNonNull(mode, "mode"); + this.entityChunkBoundaryMode = Objects.requireNonNull(entityChunkBoundaryMode, + "entityChunkBoundaryMode"); + this.nativeVoxelCollisionEnabled = nativeVoxelCollisionEnabled; + this.radius = radius; + this.bodyRadius = bodyRadius; + this.ttlTicks = ttlTicks; + } + + @Nonnull + public PhysicsChunkCollisionMode getMode() { + return mode; + } + + public void setMode(@Nonnull PhysicsChunkCollisionMode mode) { + this.mode = Objects.requireNonNull(mode, "mode"); + } + + @Nonnull + public EntityChunkBoundaryMode getEntityChunkBoundaryMode() { + return entityChunkBoundaryMode; + } + + public void setEntityChunkBoundaryMode( + @Nonnull EntityChunkBoundaryMode entityChunkBoundaryMode) { + this.entityChunkBoundaryMode = Objects.requireNonNull(entityChunkBoundaryMode, + "entityChunkBoundaryMode"); + } + + public boolean isNativeVoxelCollisionEnabled() { + return nativeVoxelCollisionEnabled; + } + + public void setNativeVoxelCollisionEnabled(boolean nativeVoxelCollisionEnabled) { + this.nativeVoxelCollisionEnabled = nativeVoxelCollisionEnabled; + } + + public int getRadius() { + return radius; + } + + public void setRadius(int radius) { + this.radius = radius; + } + + public int getBodyRadius() { + return bodyRadius; + } + + public void setBodyRadius(int bodyRadius) { + this.bodyRadius = bodyRadius; + } + + public int getTtlTicks() { + return ttlTicks; + } + + public void setTtlTicks(int ttlTicks) { + this.ttlTicks = ttlTicks; + } + + public void copyTo(@Nonnull PhysicsChunkCollisionSettings settings) { + settings.setMode(mode); + settings.setEntityChunkBoundaryMode(entityChunkBoundaryMode); + settings.setNativeVoxelCollisionEnabled(nativeVoxelCollisionEnabled); + settings.setRadius(radius); + settings.setBodyRadius(bodyRadius); + settings.setTtlTicks(ttlTicks); + } + + public boolean isDefault() { + return mode == PhysicsChunkCollisionMode.NONE + && entityChunkBoundaryMode + == PhysicsChunkCollisionSettings.DEFAULT_ENTITY_CHUNK_BOUNDARY_MODE + && nativeVoxelCollisionEnabled + == PhysicsChunkCollisionSettings.DEFAULT_NATIVE_VOXEL_COLLISION_ENABLED + && radius == PhysicsChunkCollisionSettings.DEFAULT_RADIUS + && bodyRadius == PhysicsChunkCollisionSettings.DEFAULT_BODY_RADIUS + && ttlTicks == PhysicsChunkCollisionSettings.DEFAULT_TTL_TICKS; + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.chunkCollisionSettingsComponentType(); + } + + @Nonnull + @Override + public ChunkCollisionSettingsComponent clone() { + return new ChunkCollisionSettingsComponent(mode, + entityChunkBoundaryMode, + nativeVoxelCollisionEnabled, + radius, + bodyRadius, + ttlTicks); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/components/CollisionLodSettingsComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/components/CollisionLodSettingsComponent.java new file mode 100644 index 00000000..f650c7ea --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/components/CollisionLodSettingsComponent.java @@ -0,0 +1,160 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.components.PhysicsComponentTypes; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsCollisionLodSettings; +import javax.annotation.Nonnull; + +/** + * Authored collision LOD policy for one PhysicsStore space entity. + */ +public final class CollisionLodSettingsComponent implements Component { + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + CollisionLodSettingsComponent.class, + CollisionLodSettingsComponent::new) + .append(new KeyedCodec<>("CollisionLodEnabled", Codec.BOOLEAN, false), + (component, value) -> component.collisionLodEnabled = value != null + ? value + : PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_ENABLED, + CollisionLodSettingsComponent::isCollisionLodEnabled) + .add() + .append(new KeyedCodec<>("CollisionLodNearRadius", Codec.INTEGER, false), + (component, value) -> component.collisionLodNearRadius = value != null + ? value + : PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_NEAR_RADIUS, + CollisionLodSettingsComponent::getCollisionLodNearRadius) + .add() + .append(new KeyedCodec<>("CollisionLodMidRadius", Codec.INTEGER, false), + (component, value) -> component.collisionLodMidRadius = value != null + ? value + : PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_MID_RADIUS, + CollisionLodSettingsComponent::getCollisionLodMidRadius) + .add() + .append(new KeyedCodec<>("CollisionLodHysteresis", Codec.INTEGER, false), + (component, value) -> component.collisionLodHysteresis = value != null + ? value + : PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_HYSTERESIS, + CollisionLodSettingsComponent::getCollisionLodHysteresis) + .add() + .append(new KeyedCodec<>("CollisionLodRefreshIntervalTicks", Codec.INTEGER, false), + (component, value) -> component.collisionLodRefreshIntervalTicks = value != null + ? value + : PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_REFRESH_INTERVAL_TICKS, + CollisionLodSettingsComponent::getCollisionLodRefreshIntervalTicks) + .add() + .append(new KeyedCodec<>("CollisionLodFarSleepEnabled", Codec.BOOLEAN, false), + (component, value) -> component.collisionLodFarSleepEnabled = value != null + ? value + : PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_FAR_SLEEP_ENABLED, + CollisionLodSettingsComponent::isCollisionLodFarSleepEnabled) + .add() + .build(); + + private boolean collisionLodEnabled = + PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_ENABLED; + private int collisionLodNearRadius = + PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_NEAR_RADIUS; + private int collisionLodMidRadius = + PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_MID_RADIUS; + private int collisionLodHysteresis = + PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_HYSTERESIS; + private int collisionLodRefreshIntervalTicks = + PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_REFRESH_INTERVAL_TICKS; + private boolean collisionLodFarSleepEnabled = + PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_FAR_SLEEP_ENABLED; + + public CollisionLodSettingsComponent() { + } + + public CollisionLodSettingsComponent(@Nonnull PhysicsCollisionLodSettings settings) { + collisionLodEnabled = settings.isCollisionLodEnabled(); + collisionLodNearRadius = settings.getCollisionLodNearRadius(); + collisionLodMidRadius = settings.getCollisionLodMidRadius(); + collisionLodHysteresis = settings.getCollisionLodHysteresis(); + collisionLodRefreshIntervalTicks = settings.getCollisionLodRefreshIntervalTicks(); + collisionLodFarSleepEnabled = settings.isCollisionLodFarSleepEnabled(); + } + + private CollisionLodSettingsComponent(boolean collisionLodEnabled, + int collisionLodNearRadius, + int collisionLodMidRadius, + int collisionLodHysteresis, + int collisionLodRefreshIntervalTicks, + boolean collisionLodFarSleepEnabled) { + this.collisionLodEnabled = collisionLodEnabled; + this.collisionLodNearRadius = collisionLodNearRadius; + this.collisionLodMidRadius = collisionLodMidRadius; + this.collisionLodHysteresis = collisionLodHysteresis; + this.collisionLodRefreshIntervalTicks = collisionLodRefreshIntervalTicks; + this.collisionLodFarSleepEnabled = collisionLodFarSleepEnabled; + } + + public boolean isCollisionLodEnabled() { + return collisionLodEnabled; + } + + public int getCollisionLodNearRadius() { + return collisionLodNearRadius; + } + + public int getCollisionLodMidRadius() { + return collisionLodMidRadius; + } + + public int getCollisionLodHysteresis() { + return collisionLodHysteresis; + } + + public int getCollisionLodRefreshIntervalTicks() { + return collisionLodRefreshIntervalTicks; + } + + public boolean isCollisionLodFarSleepEnabled() { + return collisionLodFarSleepEnabled; + } + + public void copyTo(@Nonnull PhysicsCollisionLodSettings target) { + target.setCollisionLodEnabled(collisionLodEnabled); + target.setCollisionLodRadii(collisionLodNearRadius, collisionLodMidRadius); + target.setCollisionLodHysteresis(collisionLodHysteresis); + target.setCollisionLodRefreshIntervalTicks(collisionLodRefreshIntervalTicks); + target.setCollisionLodFarSleepEnabled(collisionLodFarSleepEnabled); + } + + public boolean isDefault() { + return collisionLodEnabled == PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_ENABLED + && collisionLodNearRadius + == PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_NEAR_RADIUS + && collisionLodMidRadius + == PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_MID_RADIUS + && collisionLodHysteresis + == PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_HYSTERESIS + && collisionLodRefreshIntervalTicks + == PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_REFRESH_INTERVAL_TICKS + && collisionLodFarSleepEnabled + == PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_FAR_SLEEP_ENABLED; + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.collisionLodSettingsComponentType(); + } + + @Nonnull + @Override + public CollisionLodSettingsComponent clone() { + return new CollisionLodSettingsComponent(collisionLodEnabled, + collisionLodNearRadius, + collisionLodMidRadius, + collisionLodHysteresis, + collisionLodRefreshIntervalTicks, + collisionLodFarSleepEnabled); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/EntityChunkBoundaryMode.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/settings/EntityChunkBoundaryMode.java similarity index 88% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/EntityChunkBoundaryMode.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/settings/EntityChunkBoundaryMode.java index 68ff3161..d55d3e85 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/EntityChunkBoundaryMode.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/settings/EntityChunkBoundaryMode.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.plugin.settings; +package dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings; /** diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/settings/PhysicsChunkCollisionSettings.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/settings/PhysicsChunkCollisionSettings.java new file mode 100644 index 00000000..4e837fa7 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/settings/PhysicsChunkCollisionSettings.java @@ -0,0 +1,124 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings; + +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.EntityChunkBoundaryMode; +import lombok.Getter; +import lombok.Setter; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * Chunk-collision streaming settings for a PhysicsStore space. + */ +public class PhysicsChunkCollisionSettings { + + /** + * Block radius around each tracked player for streaming chunk collision bodies. + */ + public static final int DEFAULT_RADIUS = 8; + + /** + * Hard block-radius cap for player-centered PhysicsChunk collision streaming. + */ + public static final int MAX_RADIUS = 128; + + /** + * Block radius around each active dynamic physics body for streaming chunk collision bodies. + */ + public static final int DEFAULT_BODY_RADIUS = 4; + + /** + * Hard block-radius cap for dynamic-body PhysicsChunk collision streaming. + */ + public static final int MAX_BODY_RADIUS = 64; + + /** + * Ticks before an unused section's chunk collision bodies are pruned. + */ + public static final int DEFAULT_TTL_TICKS = 100; + + /** + * Hard tick cap for retaining unused streamed chunk-collision sections. + */ + public static final int MAX_TTL_TICKS = 12_000; + + /** + * Default behavior when an entity-backed body reaches an unloaded chunk border. + */ + @Nonnull + public static final EntityChunkBoundaryMode DEFAULT_ENTITY_CHUNK_BOUNDARY_MODE = + EntityChunkBoundaryMode.PAUSE_UNTIL_LOADED; + + /** + * Whether full-cube world sections should use native backend voxel collision when available. + */ + public static final boolean DEFAULT_NATIVE_VOXEL_COLLISION_ENABLED = false; + + @Nonnull + private PhysicsChunkCollisionMode mode = PhysicsChunkCollisionMode.NONE; + @Nonnull + private EntityChunkBoundaryMode entityChunkBoundaryMode = DEFAULT_ENTITY_CHUNK_BOUNDARY_MODE; + @Setter + @Getter + private boolean nativeVoxelCollisionEnabled = DEFAULT_NATIVE_VOXEL_COLLISION_ENABLED; + @Getter + private int radius = DEFAULT_RADIUS; + @Getter + private int bodyRadius = DEFAULT_BODY_RADIUS; + @Getter + private int ttlTicks = DEFAULT_TTL_TICKS; + + public PhysicsChunkCollisionSettings() { + } + + public PhysicsChunkCollisionSettings(@Nonnull PhysicsChunkCollisionSettings settings) { + mode = settings.mode; + entityChunkBoundaryMode = settings.entityChunkBoundaryMode; + nativeVoxelCollisionEnabled = settings.nativeVoxelCollisionEnabled; + radius = settings.radius; + bodyRadius = settings.bodyRadius; + ttlTicks = settings.ttlTicks; + } + + @Nonnull + public PhysicsChunkCollisionMode getMode() { + return mode; + } + + public void setMode(@Nonnull PhysicsChunkCollisionMode mode) { + this.mode = Objects.requireNonNull(mode, "mode"); + } + + @Nonnull + public EntityChunkBoundaryMode getEntityChunkBoundaryMode() { + return entityChunkBoundaryMode; + } + + public void setEntityChunkBoundaryMode( + @Nonnull EntityChunkBoundaryMode entityChunkBoundaryMode) { + this.entityChunkBoundaryMode = Objects.requireNonNull(entityChunkBoundaryMode, + "entityChunkBoundaryMode"); + } + + public void setRadius(int radius) { + this.radius = PhysicsChunkSettingsValidation.requirePositiveAtMost( + "PhysicsChunk collision radius", + radius, + MAX_RADIUS); + } + + public void setBodyRadius(int bodyRadius) { + this.bodyRadius = PhysicsChunkSettingsValidation.requirePositiveAtMost( + "PhysicsChunk collision body radius", + bodyRadius, + MAX_BODY_RADIUS); + } + + public void setTtlTicks(int ttlTicks) { + this.ttlTicks = PhysicsChunkSettingsValidation.requirePositiveAtMost( + "PhysicsChunk collision TTL", + ttlTicks, + MAX_TTL_TICKS); + } + +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/settings/PhysicsChunkSettingsValidation.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/settings/PhysicsChunkSettingsValidation.java new file mode 100644 index 00000000..a9ba9625 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/settings/PhysicsChunkSettingsValidation.java @@ -0,0 +1,23 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings; + +import javax.annotation.Nonnull; + +final class PhysicsChunkSettingsValidation { + + private PhysicsChunkSettingsValidation() { + } + + static int requirePositiveAtMost(@Nonnull String label, int value, int maxValue) { + if (value < 1 || value > maxValue) { + throw new IllegalArgumentException(label + " must be between 1 and " + maxValue); + } + return value; + } + + static float requireFiniteAtLeast(@Nonnull String label, float value, float minValue) { + if (!Float.isFinite(value) || value < minValue) { + throw new IllegalArgumentException(label + " must be finite and >= " + minValue); + } + return value; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsCollisionLodSettings.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/settings/PhysicsCollisionLodSettings.java similarity index 91% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsCollisionLodSettings.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/settings/PhysicsCollisionLodSettings.java index 0ec72e42..bd9533e1 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsCollisionLodSettings.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicschunk/settings/PhysicsCollisionLodSettings.java @@ -1,8 +1,8 @@ -package dev.hytalemodding.impulse.core.plugin.settings; +package dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings; +import javax.annotation.Nonnull; import lombok.Getter; import lombok.Setter; -import javax.annotation.Nonnull; /** * Distance-based dynamic-body collision LOD settings for a physics space. @@ -101,7 +101,7 @@ public PhysicsCollisionLodSettings(@Nonnull PhysicsCollisionLodSettings settings } public void setCollisionLodNearRadius(int collisionLodNearRadius) { - int boundedNearRadius = PhysicsSettingsValidation.requirePositiveAtMost( + int boundedNearRadius = PhysicsChunkSettingsValidation.requirePositiveAtMost( "Collision LOD near radius", collisionLodNearRadius, MAX_COLLISION_LOD_RADIUS); @@ -113,7 +113,7 @@ public void setCollisionLodNearRadius(int collisionLodNearRadius) { } public void setCollisionLodMidRadius(int collisionLodMidRadius) { - int boundedMidRadius = PhysicsSettingsValidation.requirePositiveAtMost( + int boundedMidRadius = PhysicsChunkSettingsValidation.requirePositiveAtMost( "Collision LOD mid radius", collisionLodMidRadius, MAX_COLLISION_LOD_RADIUS); @@ -126,11 +126,11 @@ public void setCollisionLodMidRadius(int collisionLodMidRadius) { public void setCollisionLodRadii(int collisionLodNearRadius, int collisionLodMidRadius) { - int boundedNearRadius = PhysicsSettingsValidation.requirePositiveAtMost( + int boundedNearRadius = PhysicsChunkSettingsValidation.requirePositiveAtMost( "Collision LOD near radius", collisionLodNearRadius, MAX_COLLISION_LOD_RADIUS); - int boundedMidRadius = PhysicsSettingsValidation.requirePositiveAtMost( + int boundedMidRadius = PhysicsChunkSettingsValidation.requirePositiveAtMost( "Collision LOD mid radius", collisionLodMidRadius, MAX_COLLISION_LOD_RADIUS); @@ -153,7 +153,7 @@ public void setCollisionLodHysteresis(int collisionLodHysteresis) { public void setCollisionLodRefreshIntervalTicks(int collisionLodRefreshIntervalTicks) { this.collisionLodRefreshIntervalTicks = - PhysicsSettingsValidation.requirePositiveAtMost( + PhysicsChunkSettingsValidation.requirePositiveAtMost( "Collision LOD refresh interval", collisionLodRefreshIntervalTicks, MAX_COLLISION_LOD_REFRESH_INTERVAL_TICKS); diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/PhysicsEntityAttachments.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/PhysicsEntityAttachments.java new file mode 100644 index 00000000..14252e5d --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/PhysicsEntityAttachments.java @@ -0,0 +1,136 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicsentity; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.PhysicsEntityLifecycle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsProjectionIndexResource; +import java.util.Collection; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Public EntityStore projection reads for PhysicsStore body attachments. + */ +public final class PhysicsEntityAttachments { + + private PhysicsEntityAttachments() { + } + + /** + * Returns whether the PhysicsEntity subplugin is loaded and its EntityStore projection types + * are registered. + */ + public static boolean isAvailable() { + return PhysicsEntityLifecycle.isEnabled() + && PhysicsEntityTypes.areEntityStoreTypesRegistered(); + } + + /** + * Requires the PhysicsEntity subplugin to be loaded before reading projection attachments. + */ + public static void requireAvailable() { + if (!isAvailable()) { + throw new IllegalStateException(PhysicsEntityLifecycle.DISABLED_MESSAGE); + } + } + + @Nonnull + public static Collection> attachments(@Nonnull Store store, + @Nonnull UUID bodyUuid) { + requireAvailable(); + assert PhysicsProjectionIndexResource.getResourceType() != null; + return requireWorldThread(store, "read PhysicsStore body attachments") + .getResource(PhysicsProjectionIndexResource.getResourceType()) + .getAttachments(Objects.requireNonNull(bodyUuid, "bodyUuid")); + } + + @Nonnull + public static Collection> attachments(@Nonnull Store store, + @Nonnull Ref bodyRef) { + requireAvailable(); + assert PhysicsProjectionIndexResource.getResourceType() != null; + return requireWorldThread(store, "read PhysicsStore body attachments") + .getResource(PhysicsProjectionIndexResource.getResourceType()) + .getAttachments(Objects.requireNonNull(bodyRef, "bodyRef")); + } + + @Nonnull + public static Collection> attachments(@Nonnull Store store, + @Nonnull UUID bodyUuid, + @Nullable Ref bodyRef) { + requireAvailable(); + assert PhysicsProjectionIndexResource.getResourceType() != null; + PhysicsProjectionIndexResource projection = + requireWorldThread(store, "read PhysicsStore body attachments") + .getResource(PhysicsProjectionIndexResource.getResourceType()); + return bodyRef != null && bodyRef.isValid() + ? projection.getAttachments(bodyRef) + : projection.getAttachments(Objects.requireNonNull(bodyUuid, "bodyUuid")); + } + + public static boolean hasAttachments(@Nonnull Store store, + @Nonnull UUID bodyUuid) { + requireAvailable(); + assert PhysicsProjectionIndexResource.getResourceType() != null; + return requireWorldThread(store, "check PhysicsStore body attachments") + .getResource(PhysicsProjectionIndexResource.getResourceType()) + .hasAttachments(Objects.requireNonNull(bodyUuid, "bodyUuid")); + } + + public static boolean hasAttachments(@Nonnull Store store, + @Nonnull Ref bodyRef) { + requireAvailable(); + assert PhysicsProjectionIndexResource.getResourceType() != null; + return requireWorldThread(store, "check PhysicsStore body attachments") + .getResource(PhysicsProjectionIndexResource.getResourceType()) + .hasAttachments(Objects.requireNonNull(bodyRef, "bodyRef")); + } + + public static boolean hasAttachments(@Nonnull Store store, + @Nonnull UUID bodyUuid, + @Nullable Ref bodyRef) { + requireAvailable(); + assert PhysicsProjectionIndexResource.getResourceType() != null; + PhysicsProjectionIndexResource projection = + requireWorldThread(store, "check PhysicsStore body attachments") + .getResource(PhysicsProjectionIndexResource.getResourceType()); + return bodyRef != null && bodyRef.isValid() + ? projection.hasAttachments(bodyRef) + : projection.hasAttachments(Objects.requireNonNull(bodyUuid, "bodyUuid")); + } + + @Nullable + public static Ref generatedVisualProxy(@Nonnull Store store, + @Nonnull UUID bodyUuid) { + requireAvailable(); + assert PhysicsProjectionIndexResource.getResourceType() != null; + return requireWorldThread(store, "read PhysicsStore generated visual proxy") + .getResource(PhysicsProjectionIndexResource.getResourceType()) + .getGeneratedVisualProxy(Objects.requireNonNull(bodyUuid, "bodyUuid")); + } + + @Nullable + public static Ref generatedVisualProxy(@Nonnull Store store, + @Nonnull Ref bodyRef) { + requireAvailable(); + assert PhysicsProjectionIndexResource.getResourceType() != null; + return requireWorldThread(store, "read PhysicsStore generated visual proxy") + .getResource(PhysicsProjectionIndexResource.getResourceType()) + .getGeneratedVisualProxy(Objects.requireNonNull(bodyRef, "bodyRef")); + } + + @Nonnull + private static Store requireWorldThread(@Nonnull Store store, + @Nonnull String operation) { + Store checkedStore = Objects.requireNonNull(store, "store"); + if (!checkedStore.getExternalData().getWorld().isInThread()) { + throw new IllegalStateException("Cannot " + operation + + " outside the owning EntityStore world thread"); + } + return checkedStore; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/PhysicsEntityDiagnostics.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/PhysicsEntityDiagnostics.java new file mode 100644 index 00000000..41ab28ec --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/PhysicsEntityDiagnostics.java @@ -0,0 +1,65 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicsentity; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import javax.annotation.Nonnull; + +/** + * Public value diagnostics for the EntityStore side of Impulse physics. + */ +public final class PhysicsEntityDiagnostics { + + private PhysicsEntityDiagnostics() { + } + + @Nonnull + public static Snapshot collect(@Nonnull Store store) { + PhysicsEntityDiagnostics.Snapshot snapshot = PhysicsEntityDiagnostics.collect(store); + return new Snapshot(snapshot.physicsBodyEntities(), + snapshot.persistentPhysicsBodyEntities(), + snapshot.physicsVisualEntities(), + snapshot.transformEntities(), + snapshot.networkIdEntities(), + snapshot.visibleEntities(), + snapshot.entityViewers(), + snapshot.physicsBodyWithTransform(), + snapshot.physicsBodyWithNetworkId(), + snapshot.physicsBodyWithVisible(), + snapshot.physicsBodyMaterialized(), + snapshot.physicsVisualMaterialized()); + } + + public record Snapshot(int physicsBodyEntities, + int persistentPhysicsBodyEntities, + int physicsVisualEntities, + int transformEntities, + int networkIdEntities, + int visibleEntities, + int entityViewers, + int physicsBodyWithTransform, + int physicsBodyWithNetworkId, + int physicsBodyWithVisible, + int physicsBodyMaterialized, + int physicsVisualMaterialized) { + + @Nonnull + public String hytaleSummary() { + return "transformEntities=" + transformEntities + + " networkIdEntities=" + networkIdEntities + + " visibleEntities=" + visibleEntities + + " entityViewers=" + entityViewers; + } + + @Nonnull + public String impulseSummary() { + return "physicsBodies=" + physicsBodyEntities + + " persistentBodies=" + persistentPhysicsBodyEntities + + " visualFollowers=" + physicsVisualEntities + + " bodyTransform=" + physicsBodyWithTransform + + " bodyNetworkId=" + physicsBodyWithNetworkId + + " bodyVisible=" + physicsBodyWithVisible + + " bodyMaterialized=" + physicsBodyMaterialized + + " visualMaterialized=" + physicsVisualMaterialized; + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/PhysicsEntityTypes.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/PhysicsEntityTypes.java new file mode 100644 index 00000000..e2bfcd39 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/PhysicsEntityTypes.java @@ -0,0 +1,56 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicsentity; + +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.SystemGroup; +import com.hypixel.hytale.component.event.WorldEventType; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.PhysicsEntityTypeRegistry; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFramePublishedEvent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.GeneratedVisualProxyComponent; +import javax.annotation.Nonnull; + +/** + * Public EntityStore type handles for the PhysicsEntity subplugin. + */ +public final class PhysicsEntityTypes { + + private PhysicsEntityTypes() { + } + + public static boolean areEntityStoreTypesRegistered() { + return PhysicsEntityTypeRegistry.areEntityStoreTypesRegistered(); + } + + public static boolean isBodyAttachmentComponentTypeRegistered() { + return PhysicsEntityTypeRegistry.isBodyAttachmentComponentTypeRegistered(); + } + + public static boolean isGeneratedVisualProxyComponentTypeRegistered() { + return PhysicsEntityTypeRegistry.isGeneratedVisualProxyComponentTypeRegistered(); + } + + @Nonnull + public static ComponentType bodyAttachmentComponentType() { + return PhysicsEntityTypeRegistry.bodyAttachmentComponentType(); + } + + @Nonnull + public static ComponentType generatedVisualProxyComponentType() { + return PhysicsEntityTypeRegistry.generatedVisualProxyComponentType(); + } + + /** + * @deprecated Physics event plugin API is deprecated without replacement. + */ + @Deprecated(since = "0.1.0", forRemoval = false) + @Nonnull + public static WorldEventType physicsEventFramePublishedEventType() { + return PhysicsEntityTypeRegistry.physicsEventFramePublishedEventType(); + } + + @Nonnull + public static SystemGroup persistenceRestoreGroup() { + return PhysicsEntityTypeRegistry.persistenceRestoreGroup(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/PhysicsRuntimeProfiling.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/PhysicsRuntimeProfiling.java new file mode 100644 index 00000000..37388e32 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/PhysicsRuntimeProfiling.java @@ -0,0 +1,367 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicsentity; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * Public value views for EntityStore-side physics runtime profiling. + */ +public final class PhysicsRuntimeProfiling { + + private PhysicsRuntimeProfiling() { + } + + @Nonnull + public static Snapshots snapshots(@Nonnull Store store) { + assert PhysicsRuntimeProfilingResource.getResourceType() != null; + PhysicsRuntimeProfilingResource profiling = store.getResource( + PhysicsRuntimeProfilingResource.getResourceType()); + return new Snapshots(profiling.isEnabled(), + new StepSnapshotView(profiling.getCumulativeStep()), + new StepSnapshotView(profiling.getLatestStep()), + new StepSnapshotView(profiling.getLatestCompletedStep()), + new StepSnapshotView(profiling.getWorstStep()), + new SyncSnapshotView(profiling.getCumulativeSync()), + new SyncSnapshotView(profiling.getLatestSync()), + new SyncSnapshotView(profiling.getWorstSync()), + new VisualSnapshotView(profiling.getCumulativeVisual()), + new VisualSnapshotView(profiling.getLatestVisual()), + new VisualSnapshotView(profiling.getWorstVisual())); + } + + public record Snapshots(boolean enabled, + @Nonnull StepSnapshotView cumulativeStep, + @Nonnull StepSnapshotView latestStep, + @Nonnull StepSnapshotView latestCompletedStep, + @Nonnull StepSnapshotView worstStep, + @Nonnull SyncSnapshotView cumulativeSync, + @Nonnull SyncSnapshotView latestSync, + @Nonnull SyncSnapshotView worstSync, + @Nonnull VisualSnapshotView cumulativeVisual, + @Nonnull VisualSnapshotView latestVisual, + @Nonnull VisualSnapshotView worstVisual) { + } + + public interface StepDrainSnapshotView { + + int getTickSamples(); + + int getPreStepDrainedMutations(); + + int getMaxPreStepDrainedMutations(); + + long getPreStepDrainRunNanos(); + + int getLateMutationBacklogAtStep(); + + int getMaxLateMutationBacklogAtStep(); + } + + public static final class StepSnapshotView implements StepDrainSnapshotView { + + @Nonnull + private final PhysicsRuntimeProfilingResource.StepSnapshot snapshot; + + private StepSnapshotView( + @Nonnull PhysicsRuntimeProfilingResource.StepSnapshot snapshot) { + this.snapshot = Objects.requireNonNull(snapshot, "snapshot"); + } + + public int getTickSamples() { + return snapshot.getTickSamples(); + } + + public int getSpaces() { + return snapshot.getSpaces(); + } + + public int getSubsteps() { + return snapshot.getSubsteps(); + } + + public int getBodySnapshots() { + return snapshot.getBodySnapshots(); + } + + public int getSpatialIndexCells() { + return snapshot.getSpatialIndexCells(); + } + + public long getTickNanos() { + return snapshot.getTickNanos(); + } + + public long getSnapshotNanos() { + return snapshot.getSnapshotNanos(); + } + + public long getStoreTickQueuedNanos() { + return snapshot.getStoreTickQueuedNanos(); + } + + public long getStoreTickRunNanos() { + return snapshot.getStoreTickRunNanos(); + } + + public int getPreStepDrainedMutations() { + return snapshot.getPreStepDrainedMutations(); + } + + public int getMaxPreStepDrainedMutations() { + return snapshot.getMaxPreStepDrainedMutations(); + } + + public long getPreStepDrainRunNanos() { + return snapshot.getPreStepDrainRunNanos(); + } + + public int getLateMutationBacklogAtStep() { + return snapshot.getLateMutationBacklogAtStep(); + } + + public int getMaxLateMutationBacklogAtStep() { + return snapshot.getMaxLateMutationBacklogAtStep(); + } + + public int getStoreTickStepRateSamples() { + return snapshot.getStoreTickStepRateSamples(); + } + + public long getStoreTickStepIntervalNanos() { + return snapshot.getStoreTickStepIntervalNanos(); + } + + public long getMaxStoreTickStepIntervalNanos() { + return snapshot.getMaxStoreTickStepIntervalNanos(); + } + + public int getSkippedPendingSteps() { + return snapshot.getSkippedPendingSteps(); + } + + public long getPendingStepAgeNanos() { + return snapshot.getPendingStepAgeNanos(); + } + + public long getMaxPendingStepAgeNanos() { + return snapshot.getMaxPendingStepAgeNanos(); + } + + public int getSchedulerSamples() { + return snapshot.getSchedulerSamples(); + } + + public long getSchedulerInputDtNanos() { + return snapshot.getSchedulerInputDtNanos(); + } + + public long getSchedulerSubmittedDtNanos() { + return snapshot.getSchedulerSubmittedDtNanos(); + } + + public long getSchedulerBacklogDtNanos() { + return snapshot.getSchedulerBacklogDtNanos(); + } + + public long getMaxSchedulerBacklogDtNanos() { + return snapshot.getMaxSchedulerBacklogDtNanos(); + } + + public long getDroppedBacklogDtNanos() { + return snapshot.getDroppedBacklogDtNanos(); + } + + public int getDroppedBacklogTicks() { + return snapshot.getDroppedBacklogTicks(); + } + + public int getDtCapHits() { + return snapshot.getDtCapHits(); + } + + public int getNativePhaseSamples() { + return snapshot.getNativePhaseSamples(); + } + + public long getNativeStepNanos() { + return snapshot.getNativeStepNanos(); + } + + public long getNativeBroadPhaseNanos() { + return snapshot.getNativeBroadPhaseNanos(); + } + + public long getNativeNarrowPhaseNanos() { + return snapshot.getNativeNarrowPhaseNanos(); + } + + public long getNativeSolverNanos() { + return snapshot.getNativeSolverNanos(); + } + + public long getNativeCcdNanos() { + return snapshot.getNativeCcdNanos(); + } + + public long getNativeSnapshotNanos() { + return snapshot.getNativeSnapshotNanos(); + } + } + + public static final class SyncSnapshotView { + + @Nonnull + private final PhysicsRuntimeProfilingResource.SyncSnapshot snapshot; + + private SyncSnapshotView( + @Nonnull PhysicsRuntimeProfilingResource.SyncSnapshot snapshot) { + this.snapshot = Objects.requireNonNull(snapshot, "snapshot"); + } + + public int getTickSamples() { + return snapshot.getTickSamples(); + } + + public int getBodiesInspected() { + return snapshot.getBodiesInspected(); + } + + public int getBodiesSynced() { + return snapshot.getBodiesSynced(); + } + + public int getTransitionSyncs() { + return snapshot.getTransitionSyncs(); + } + + public int getKeepaliveSyncs() { + return snapshot.getKeepaliveSyncs(); + } + + public int getSkippedSleeping() { + return snapshot.getSkippedSleeping(); + } + + public int getSkippedThreshold() { + return snapshot.getSkippedThreshold(); + } + + public int getSkippedVisualDeadzone() { + return snapshot.getSkippedVisualDeadzone(); + } + + public int getSkippedVisualRange() { + return snapshot.getSkippedVisualRange(); + } + + public int getSkippedStatic() { + return snapshot.getSkippedStatic(); + } + + public int getSkippedMissingSpace() { + return snapshot.getSkippedMissingSpace(); + } + + public long getTickNanos() { + return snapshot.getTickNanos(); + } + + public int getBodySnapshotMotionSamples() { + return snapshot.getBodySnapshotMotionSamples(); + } + + public double getBodySnapshotMotionDistance() { + return snapshot.getBodySnapshotMotionDistance(); + } + + public double getMaxBodySnapshotMotionDistance() { + return snapshot.getMaxBodySnapshotMotionDistance(); + } + + public int getVisualCorrectionSamples() { + return snapshot.getVisualCorrectionSamples(); + } + + public double getVisualCorrectionDistance() { + return snapshot.getVisualCorrectionDistance(); + } + + public double getMaxVisualCorrectionDistance() { + return snapshot.getMaxVisualCorrectionDistance(); + } + } + + public static final class VisualSnapshotView { + + @Nonnull + private final PhysicsRuntimeProfilingResource.VisualSnapshot snapshot; + + private VisualSnapshotView( + @Nonnull PhysicsRuntimeProfilingResource.VisualSnapshot snapshot) { + this.snapshot = Objects.requireNonNull(snapshot, "snapshot"); + } + + public int getTickSamples() { + return snapshot.getTickSamples(); + } + + public int getInterests() { + return snapshot.getInterests(); + } + + public int getMaterialized() { + return snapshot.getMaterialized(); + } + + public int getCandidates() { + return snapshot.getCandidates(); + } + + public int getSpawned() { + return snapshot.getSpawned(); + } + + public int getDematerialized() { + return snapshot.getDematerialized(); + } + + public int getNearQueries() { + return snapshot.getNearQueries(); + } + + public int getNearQueryCandidates() { + return snapshot.getNearQueryCandidates(); + } + + public int getRaycasts() { + return snapshot.getRaycasts(); + } + + public int getRaycastCacheHits() { + return snapshot.getRaycastCacheHits(); + } + + public int getCandidateRefreshes() { + return snapshot.getCandidateRefreshes(); + } + + public int getCandidateCacheUses() { + return snapshot.getCandidateCacheUses(); + } + + public int getVisibilityChecks() { + return snapshot.getVisibilityChecks(); + } + + public int getVisibilityCheckSkips() { + return snapshot.getVisibilityCheckSkips(); + } + + public long getTickNanos() { + return snapshot.getTickNanos(); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyAttachmentComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/components/BodyAttachmentComponent.java similarity index 55% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyAttachmentComponent.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/components/BodyAttachmentComponent.java index bf7f06a2..a993da0f 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyAttachmentComponent.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/components/BodyAttachmentComponent.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.plugin.components; +package dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components; import com.hypixel.hytale.codec.Codec; import com.hypixel.hytale.codec.KeyedCodec; @@ -6,12 +6,13 @@ import com.hypixel.hytale.codec.codecs.EnumCodec; import com.hypixel.hytale.component.Component; import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.math.vector.Vector3fUtil; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.core.plugin.codec.ImpulseCodecs; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsEntityTypes; +import java.util.Objects; import java.util.UUID; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -21,49 +22,39 @@ import org.joml.Vector3f; /** - * Runtime attachment from a Hytale entity to an Impulse body key. + * EntityStore projection relationship to an authoritative PhysicsStore body. * - *

The entity is a gameplay or visual representation. It does not own backend - * body destruction; removing the entity only removes this attachment unless the - * lifecycle marks it as a disposable Impulse visual.

+ * TODO: probably refactor the name */ -public class PhysicsBodyAttachmentComponent implements Component { +public class BodyAttachmentComponent implements Component { private static final float USE_BODY_VISUAL_ORIGIN_OFFSET_Y = -1.0f; @Nonnull - public static final BuilderCodec CODEC = BuilderCodec.builder( - PhysicsBodyAttachmentComponent.class, - PhysicsBodyAttachmentComponent::new) - .append(new KeyedCodec<>("BodyId", Codec.UUID_BINARY, false), - (component, value) -> component.bodyKey = value != null - ? RigidBodyKey.of(value) - : RigidBodyKey.random(), - PhysicsBodyAttachmentComponent::getBodyKeyValue) - .add() - .append(new KeyedCodec<>("SpaceId", Codec.INTEGER, false), - (component, value) -> component.spaceId = value != null && value > 0 - ? new SpaceId(value) - : null, - PhysicsBodyAttachmentComponent::getSpaceIdValue) + public static final BuilderCodec CODEC = BuilderCodec.builder( + BodyAttachmentComponent.class, + BodyAttachmentComponent::new) + .append(new KeyedCodec<>("BodyUuid", Codec.UUID_BINARY, false), + (component, value) -> component.bodyUuid = value != null ? value : UUID.randomUUID(), + BodyAttachmentComponent::getBodyUuid) .add() .append(new KeyedCodec<>("TransformAuthority", new EnumCodec<>(TransformAuthority.class), false), (component, value) -> component.transformAuthority = value != null ? value : TransformAuthority.BODY, - PhysicsBodyAttachmentComponent::getTransformAuthority) + BodyAttachmentComponent::getTransformAuthority) .add() .append(new KeyedCodec<>("Lifecycle", new EnumCodec<>(AttachmentLifecycle.class), false), (component, value) -> component.lifecycle = value != null ? value : AttachmentLifecycle.EXTERNAL_ENTITY, - PhysicsBodyAttachmentComponent::getLifecycle) + BodyAttachmentComponent::getLifecycle) .add() .append(new KeyedCodec<>("LocalPositionOffset", Vector3fUtil.CODEC, false), (component, value) -> component.localPositionOffset.set(value != null ? value : new Vector3f()), - PhysicsBodyAttachmentComponent::getLocalPositionOffset) + BodyAttachmentComponent::getLocalPositionOffset) .add() .append(new KeyedCodec<>("LocalRotationOffset", ImpulseCodecs.QUATERNIONF, false), (component, value) -> component.localRotationOffset.set(value != null @@ -73,16 +64,15 @@ public class PhysicsBodyAttachmentComponent implements Component { .add() .append(new KeyedCodec<>("VisualOriginOffsetY", Codec.FLOAT, false), (component, value) -> component.visualOriginOffsetY = normalizeVisualOriginOffsetY(value), - PhysicsBodyAttachmentComponent::getVisualOriginOffsetY) + BodyAttachmentComponent::getVisualOriginOffsetY) .add() .build(); - private RigidBodyKey bodyKey = RigidBodyKey.random(); + @Nonnull + private UUID bodyUuid = UUID.randomUUID(); - @Setter - @Getter @Nullable - private SpaceId spaceId; + private transient Ref bodyRef; @Setter private TransformAuthority transformAuthority = TransformAuthority.BODY; @@ -98,36 +88,33 @@ public class PhysicsBodyAttachmentComponent implements Component { @Getter private final Quaternionf localRotationOffset = new Quaternionf(); + // TODO: static/final? + @Getter private float visualOriginOffsetY = USE_BODY_VISUAL_ORIGIN_OFFSET_Y; - public PhysicsBodyAttachmentComponent() { + public BodyAttachmentComponent() { } - public PhysicsBodyAttachmentComponent(@Nonnull RigidBodyKey bodyKey, - @Nullable SpaceId spaceId) { - this(bodyKey, - spaceId, + public BodyAttachmentComponent(@Nonnull UUID bodyUuid) { + this(bodyUuid, TransformAuthority.BODY, AttachmentLifecycle.EXTERNAL_ENTITY, new Vector3f(), new Quaternionf()); } - public PhysicsBodyAttachmentComponent(@Nonnull RigidBodyKey bodyKey, - @Nullable SpaceId spaceId, + public BodyAttachmentComponent(@Nonnull UUID bodyUuid, @Nonnull TransformAuthority transformAuthority, @Nonnull AttachmentLifecycle lifecycle) { - this(bodyKey, spaceId, transformAuthority, lifecycle, new Vector3f(), new Quaternionf()); + this(bodyUuid, transformAuthority, lifecycle, new Vector3f(), new Quaternionf()); } - public PhysicsBodyAttachmentComponent(@Nonnull RigidBodyKey bodyKey, - @Nullable SpaceId spaceId, + public BodyAttachmentComponent(@Nonnull UUID bodyUuid, @Nonnull TransformAuthority transformAuthority, @Nonnull AttachmentLifecycle lifecycle, @Nonnull Vector3f localPositionOffset, @Nonnull Quaternionf localRotationOffset) { - this(bodyKey, - spaceId, + this(bodyUuid, transformAuthority, lifecycle, localPositionOffset, @@ -135,64 +122,46 @@ public PhysicsBodyAttachmentComponent(@Nonnull RigidBodyKey bodyKey, USE_BODY_VISUAL_ORIGIN_OFFSET_Y); } - public PhysicsBodyAttachmentComponent(@Nonnull RigidBodyKey bodyKey, - @Nullable SpaceId spaceId, + public BodyAttachmentComponent(@Nonnull UUID bodyUuid, @Nonnull TransformAuthority transformAuthority, @Nonnull AttachmentLifecycle lifecycle, @Nonnull Vector3f localPositionOffset, @Nonnull Quaternionf localRotationOffset, float visualOriginOffsetY) { - this.bodyKey = bodyKey; - this.spaceId = spaceId; - this.transformAuthority = transformAuthority; - this.lifecycle = lifecycle; - this.localPositionOffset.set(localPositionOffset); - this.localRotationOffset.set(localRotationOffset); + this.bodyUuid = Objects.requireNonNull(bodyUuid, "bodyUuid"); + this.transformAuthority = Objects.requireNonNull(transformAuthority, "transformAuthority"); + this.lifecycle = Objects.requireNonNull(lifecycle, "lifecycle"); + this.localPositionOffset.set(Objects.requireNonNull(localPositionOffset, + "localPositionOffset")); + this.localRotationOffset.set(Objects.requireNonNull(localRotationOffset, + "localRotationOffset")); this.visualOriginOffsetY = normalizeVisualOriginOffsetY(visualOriginOffsetY); } - /** - * Creates the normal attachment for a plugin-owned gameplay or visual entity. - * - *

The entity follows the body but does not own backend body destruction.

- */ @Nonnull - public static PhysicsBodyAttachmentComponent externalEntity(@Nonnull RigidBodyKey bodyKey, - @Nullable SpaceId spaceId) { - return new PhysicsBodyAttachmentComponent(bodyKey, - spaceId, + public static BodyAttachmentComponent externalEntity(@Nonnull UUID bodyUuid) { + return new BodyAttachmentComponent(bodyUuid, TransformAuthority.BODY, AttachmentLifecycle.EXTERNAL_ENTITY); } - /** - * Creates the normal external-entity attachment with a local transform offset. - */ @Nonnull - public static PhysicsBodyAttachmentComponent externalEntity(@Nonnull RigidBodyKey bodyKey, - @Nullable SpaceId spaceId, + public static BodyAttachmentComponent externalEntity(@Nonnull UUID bodyUuid, @Nonnull Vector3f localPositionOffset, @Nonnull Quaternionf localRotationOffset) { - return new PhysicsBodyAttachmentComponent(bodyKey, - spaceId, + return new BodyAttachmentComponent(bodyUuid, TransformAuthority.BODY, AttachmentLifecycle.EXTERNAL_ENTITY, localPositionOffset, localRotationOffset); } - /** - * Creates an external-entity attachment whose visual transform origin differs from the owning - * body's support/base offset. - */ @Nonnull - public static PhysicsBodyAttachmentComponent externalEntity(@Nonnull RigidBodyKey bodyKey, - @Nullable SpaceId spaceId, + public static BodyAttachmentComponent externalEntity(@Nonnull UUID bodyUuid, @Nonnull Vector3f localPositionOffset, @Nonnull Quaternionf localRotationOffset, float visualOriginOffsetY) { - return new PhysicsBodyAttachmentComponent(bodyKey, - spaceId, + return new BodyAttachmentComponent(bodyUuid, TransformAuthority.BODY, AttachmentLifecycle.EXTERNAL_ENTITY, localPositionOffset, @@ -200,20 +169,12 @@ public static PhysicsBodyAttachmentComponent externalEntity(@Nonnull RigidBodyKe visualOriginOffsetY); } - /** - * Creates a disposable Impulse-owned visual attachment. - * - *

Unlike {@link #externalEntity(RigidBodyKey, SpaceId)}, this entity should be removed when - * the attached body is no longer available.

- */ @Nonnull - public static PhysicsBodyAttachmentComponent impulseOwnedVisual(@Nonnull RigidBodyKey bodyKey, - @Nullable SpaceId spaceId, + public static BodyAttachmentComponent impulseOwnedVisual(@Nonnull UUID bodyUuid, @Nonnull Vector3f localPositionOffset, @Nonnull Quaternionf localRotationOffset, float visualOriginOffsetY) { - return new PhysicsBodyAttachmentComponent(bodyKey, - spaceId, + return new BodyAttachmentComponent(bodyUuid, TransformAuthority.BODY, AttachmentLifecycle.IMPULSE_OWNED_VISUAL, localPositionOffset, @@ -222,12 +183,22 @@ public static PhysicsBodyAttachmentComponent impulseOwnedVisual(@Nonnull RigidBo } @Nonnull - public RigidBodyKey getBodyKey() { - return bodyKey; + public UUID getBodyUuid() { + return bodyUuid; } - public void setBodyKey(@Nonnull RigidBodyKey bodyKey) { - this.bodyKey = bodyKey; + public void setBodyUuid(@Nonnull UUID bodyUuid) { + this.bodyUuid = Objects.requireNonNull(bodyUuid, "bodyUuid"); + bodyRef = null; + } + + @Nullable + public Ref getBodyRef() { + return bodyRef; + } + + public void setBodyRef(@Nullable Ref bodyRef) { + this.bodyRef = bodyRef; } @Nonnull @@ -240,10 +211,6 @@ public AttachmentLifecycle getLifecycle() { return lifecycle; } - public float getVisualOriginOffsetY() { - return visualOriginOffsetY; - } - public void setVisualOriginOffsetY(float visualOriginOffsetY) { this.visualOriginOffsetY = normalizeVisualOriginOffsetY(visualOriginOffsetY); } @@ -257,30 +224,26 @@ public boolean shouldRemoveEntityWhenBodyMissing() { || lifecycle == AttachmentLifecycle.GENERATED_PROXY; } - public static ComponentType getComponentType() { - return ImpulsePlugin.get().getPhysicsBodyAttachmentComponentType(); + public static boolean isComponentTypeRegistered() { + return PhysicsEntityTypes.isBodyAttachmentComponentTypeRegistered(); } @Nonnull - private UUID getBodyKeyValue() { - return bodyKey.value(); - } - - @Nullable - private Integer getSpaceIdValue() { - return spaceId != null ? spaceId.value() : null; + public static ComponentType getComponentType() { + return PhysicsEntityTypes.bodyAttachmentComponentType(); } @Nonnull @Override - public PhysicsBodyAttachmentComponent clone() { - return new PhysicsBodyAttachmentComponent(bodyKey, - spaceId, + public BodyAttachmentComponent clone() { + BodyAttachmentComponent copy = new BodyAttachmentComponent(bodyUuid, transformAuthority, lifecycle, localPositionOffset, localRotationOffset, visualOriginOffsetY); + copy.bodyRef = bodyRef; + return copy; } private static float normalizeVisualOriginOffsetY(@Nullable Float value) { @@ -294,17 +257,18 @@ private static float normalizeVisualOriginOffsetY(float value) { return normalizeVisualOriginOffsetY(Float.valueOf(value)); } + // TODO: rework and think deeply if we can just infer ownership based public enum TransformAuthority { - /* - * TODO: Revisit transform ownership when multi-body actor wrappers land. - * A root actor may need to own gameplay transforms while individual - * bodies still publish resolved physics poses. - */ BODY, CONTROLLER, ENTITY_KINEMATIC } + /** + * @deprecated AttachmentLifecycle is basically legacy stuff, the lifecycle is trivial given the + * relationship with another Store or not. + */ + @Deprecated(forRemoval = true) public enum AttachmentLifecycle { EXTERNAL_ENTITY, IMPULSE_OWNED_VISUAL, diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/components/GeneratedVisualProxyComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/components/GeneratedVisualProxyComponent.java similarity index 58% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/components/GeneratedVisualProxyComponent.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/components/GeneratedVisualProxyComponent.java index 0fbad7c6..3e600ba1 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/internal/components/GeneratedVisualProxyComponent.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/components/GeneratedVisualProxyComponent.java @@ -1,15 +1,20 @@ -package dev.hytalemodding.impulse.core.internal.components; +package dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components; import com.hypixel.hytale.codec.builder.BuilderCodec; import com.hypixel.hytale.component.Component; import com.hypixel.hytale.component.ComponentType; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.ImpulsePlugin; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsEntityTypes; import javax.annotation.Nonnull; /** * Durable ownership marker for Impulse-generated visual proxy entities. + * + * @deprecated This follows the old physics entity ownership model, the TransformComponent of the + * entities spawned with BodyAttachmentComponent is by definition owned by the bound PhysicsStore + * entity anyway */ +@Deprecated (forRemoval = true) public final class GeneratedVisualProxyComponent implements Component { @Nonnull @@ -18,8 +23,13 @@ public final class GeneratedVisualProxyComponent implements Component getComponentType() { - return ImpulsePlugin.get().getGeneratedVisualProxyComponentType(); + return PhysicsEntityTypes.generatedVisualProxyComponentType(); } @Nonnull diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/components/VisualMaterializationSettingsComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/components/VisualMaterializationSettingsComponent.java new file mode 100644 index 00000000..ce0486ab --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/components/VisualMaterializationSettingsComponent.java @@ -0,0 +1,232 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.components.PhysicsComponentTypes; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualMaterializationSettings; +import java.util.Objects; +import javax.annotation.Nonnull; +import lombok.Getter; + +/** + * Authored detached visual materialization policy for one PhysicsStore space entity. + */ +public final class VisualMaterializationSettingsComponent implements Component { + + @Nonnull + public static final BuilderCodec CODEC = + BuilderCodec.builder(VisualMaterializationSettingsComponent.class, + VisualMaterializationSettingsComponent::new) + .append(new KeyedCodec<>("DetachedVisualMaterializationEnabled", Codec.BOOLEAN, false), + (component, value) -> component.detachedVisualMaterializationEnabled = + value != null + ? value + : PhysicsVisualMaterializationSettings + .DEFAULT_DETACHED_VISUAL_MATERIALIZATION_ENABLED, + VisualMaterializationSettingsComponent::isDetachedVisualMaterializationEnabled) + .add() + .append(new KeyedCodec<>("DetachedVisualMaterializationRadius", Codec.INTEGER, false), + (component, value) -> component.detachedVisualMaterializationRadius = + value != null + ? value + : PhysicsVisualMaterializationSettings + .DEFAULT_DETACHED_VISUAL_MATERIALIZATION_RADIUS, + VisualMaterializationSettingsComponent::getDetachedVisualMaterializationRadius) + .add() + .append(new KeyedCodec<>("DetachedVisualDematerializationRadius", Codec.INTEGER, false), + (component, value) -> component.detachedVisualDematerializationRadius = + value != null + ? value + : PhysicsVisualMaterializationSettings + .DEFAULT_DETACHED_VISUAL_DEMATERIALIZATION_RADIUS, + VisualMaterializationSettingsComponent::getDetachedVisualDematerializationRadius) + .add() + .append(new KeyedCodec<>("DetachedVisualMaxSpawnsPerTick", Codec.INTEGER, false), + (component, value) -> component.detachedVisualMaxSpawnsPerTick = value != null + ? value + : PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_MAX_SPAWNS_PER_TICK, + VisualMaterializationSettingsComponent::getDetachedVisualMaxSpawnsPerTick) + .add() + .append(new KeyedCodec<>("DetachedVisualMaxMaterialized", Codec.INTEGER, false), + (component, value) -> component.detachedVisualMaxMaterialized = value != null + ? value + : PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_MAX_MATERIALIZED, + VisualMaterializationSettingsComponent::getDetachedVisualMaxMaterialized) + .add() + .append(new KeyedCodec<>("DetachedVisualInterestRefreshIntervalTicks", Codec.INTEGER, false), + (component, value) -> component.detachedVisualInterestRefreshIntervalTicks = + value != null + ? value + : PhysicsVisualMaterializationSettings + .DEFAULT_DETACHED_VISUAL_INTEREST_REFRESH_INTERVAL_TICKS, + VisualMaterializationSettingsComponent::getDetachedVisualInterestRefreshIntervalTicks) + .add() + .append(new KeyedCodec<>("DetachedVisualCandidateRefreshIntervalTicks", Codec.INTEGER, false), + (component, value) -> component.detachedVisualCandidateRefreshIntervalTicks = + value != null + ? value + : PhysicsVisualMaterializationSettings + .DEFAULT_DETACHED_VISUAL_CANDIDATE_REFRESH_INTERVAL_TICKS, + VisualMaterializationSettingsComponent::getDetachedVisualCandidateRefreshIntervalTicks) + .add() + .append(new KeyedCodec<>("DetachedVisualVisibilityCheckIntervalTicks", Codec.INTEGER, false), + (component, value) -> component.detachedVisualVisibilityCheckIntervalTicks = + value != null + ? value + : PhysicsVisualMaterializationSettings + .DEFAULT_DETACHED_VISUAL_VISIBILITY_CHECK_INTERVAL_TICKS, + VisualMaterializationSettingsComponent::getDetachedVisualVisibilityCheckIntervalTicks) + .add() + .append(new KeyedCodec<>("DetachedVisualBlockType", Codec.STRING, false), + (component, value) -> component.detachedVisualBlockType = value != null + ? value + : PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_BLOCK_TYPE, + VisualMaterializationSettingsComponent::getDetachedVisualBlockType) + .add() + .build(); + + @Getter + private boolean detachedVisualMaterializationEnabled = + PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_MATERIALIZATION_ENABLED; + @Getter + private int detachedVisualMaterializationRadius = + PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_MATERIALIZATION_RADIUS; + @Getter + private int detachedVisualDematerializationRadius = + PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_DEMATERIALIZATION_RADIUS; + @Getter + private int detachedVisualMaxSpawnsPerTick = + PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_MAX_SPAWNS_PER_TICK; + @Getter + private int detachedVisualMaxMaterialized = + PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_MAX_MATERIALIZED; + @Getter + private int detachedVisualInterestRefreshIntervalTicks = + PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_INTEREST_REFRESH_INTERVAL_TICKS; + @Getter + private int detachedVisualCandidateRefreshIntervalTicks = + PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_CANDIDATE_REFRESH_INTERVAL_TICKS; + @Getter + private int detachedVisualVisibilityCheckIntervalTicks = + PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_VISIBILITY_CHECK_INTERVAL_TICKS; + @Nonnull + private String detachedVisualBlockType = + PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_BLOCK_TYPE; + + public VisualMaterializationSettingsComponent() { + } + + public VisualMaterializationSettingsComponent( + @Nonnull PhysicsVisualMaterializationSettings settings) { + detachedVisualMaterializationEnabled = + settings.isDetachedVisualMaterializationEnabled(); + detachedVisualMaterializationRadius = + settings.getDetachedVisualMaterializationRadius(); + detachedVisualDematerializationRadius = + settings.getDetachedVisualDematerializationRadius(); + detachedVisualMaxSpawnsPerTick = settings.getDetachedVisualMaxSpawnsPerTick(); + detachedVisualMaxMaterialized = settings.getDetachedVisualMaxMaterialized(); + detachedVisualInterestRefreshIntervalTicks = + settings.getDetachedVisualInterestRefreshIntervalTicks(); + detachedVisualCandidateRefreshIntervalTicks = + settings.getDetachedVisualCandidateRefreshIntervalTicks(); + detachedVisualVisibilityCheckIntervalTicks = + settings.getDetachedVisualVisibilityCheckIntervalTicks(); + detachedVisualBlockType = settings.getDetachedVisualBlockType(); + } + + private VisualMaterializationSettingsComponent(boolean detachedVisualMaterializationEnabled, + int detachedVisualMaterializationRadius, + int detachedVisualDematerializationRadius, + int detachedVisualMaxSpawnsPerTick, + int detachedVisualMaxMaterialized, + int detachedVisualInterestRefreshIntervalTicks, + int detachedVisualCandidateRefreshIntervalTicks, + int detachedVisualVisibilityCheckIntervalTicks, + @Nonnull String detachedVisualBlockType) { + this.detachedVisualMaterializationEnabled = detachedVisualMaterializationEnabled; + this.detachedVisualMaterializationRadius = detachedVisualMaterializationRadius; + this.detachedVisualDematerializationRadius = detachedVisualDematerializationRadius; + this.detachedVisualMaxSpawnsPerTick = detachedVisualMaxSpawnsPerTick; + this.detachedVisualMaxMaterialized = detachedVisualMaxMaterialized; + this.detachedVisualInterestRefreshIntervalTicks = detachedVisualInterestRefreshIntervalTicks; + this.detachedVisualCandidateRefreshIntervalTicks = + detachedVisualCandidateRefreshIntervalTicks; + this.detachedVisualVisibilityCheckIntervalTicks = + detachedVisualVisibilityCheckIntervalTicks; + this.detachedVisualBlockType = Objects.requireNonNull(detachedVisualBlockType, + "detachedVisualBlockType"); + } + + @Nonnull + public String getDetachedVisualBlockType() { + return detachedVisualBlockType; + } + + public void copyTo(@Nonnull PhysicsVisualMaterializationSettings target) { + target.setDetachedVisualMaterializationEnabled(detachedVisualMaterializationEnabled); + target.setDetachedVisualRadii(detachedVisualMaterializationRadius, + detachedVisualDematerializationRadius); + target.setDetachedVisualMaxSpawnsPerTick(detachedVisualMaxSpawnsPerTick); + target.setDetachedVisualMaxMaterialized(detachedVisualMaxMaterialized); + target.setDetachedVisualInterestRefreshIntervalTicks( + detachedVisualInterestRefreshIntervalTicks); + target.setDetachedVisualCandidateRefreshIntervalTicks( + detachedVisualCandidateRefreshIntervalTicks); + target.setDetachedVisualVisibilityCheckIntervalTicks( + detachedVisualVisibilityCheckIntervalTicks); + target.setDetachedVisualBlockType(detachedVisualBlockType); + } + + public boolean isDefault() { + return detachedVisualMaterializationEnabled + == PhysicsVisualMaterializationSettings + .DEFAULT_DETACHED_VISUAL_MATERIALIZATION_ENABLED + && detachedVisualMaterializationRadius + == PhysicsVisualMaterializationSettings + .DEFAULT_DETACHED_VISUAL_MATERIALIZATION_RADIUS + && detachedVisualDematerializationRadius + == PhysicsVisualMaterializationSettings + .DEFAULT_DETACHED_VISUAL_DEMATERIALIZATION_RADIUS + && detachedVisualMaxSpawnsPerTick + == PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_MAX_SPAWNS_PER_TICK + && detachedVisualMaxMaterialized + == PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_MAX_MATERIALIZED + && detachedVisualInterestRefreshIntervalTicks + == PhysicsVisualMaterializationSettings + .DEFAULT_DETACHED_VISUAL_INTEREST_REFRESH_INTERVAL_TICKS + && detachedVisualCandidateRefreshIntervalTicks + == PhysicsVisualMaterializationSettings + .DEFAULT_DETACHED_VISUAL_CANDIDATE_REFRESH_INTERVAL_TICKS + && detachedVisualVisibilityCheckIntervalTicks + == PhysicsVisualMaterializationSettings + .DEFAULT_DETACHED_VISUAL_VISIBILITY_CHECK_INTERVAL_TICKS + && detachedVisualBlockType.equals( + PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_BLOCK_TYPE); + } + + @Nonnull + public static ComponentType + getComponentType() { + return PhysicsComponentTypes.visualMaterializationSettingsComponentType(); + } + + @Nonnull + @Override + public VisualMaterializationSettingsComponent clone() { + return new VisualMaterializationSettingsComponent( + detachedVisualMaterializationEnabled, + detachedVisualMaterializationRadius, + detachedVisualDematerializationRadius, + detachedVisualMaxSpawnsPerTick, + detachedVisualMaxMaterialized, + detachedVisualInterestRefreshIntervalTicks, + detachedVisualCandidateRefreshIntervalTicks, + detachedVisualVisibilityCheckIntervalTicks, + detachedVisualBlockType); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/components/VisualSyncSettingsComponent.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/components/VisualSyncSettingsComponent.java new file mode 100644 index 00000000..dc30bac1 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/components/VisualSyncSettingsComponent.java @@ -0,0 +1,280 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.codec.codecs.EnumCodec; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.components.PhysicsComponentTypes; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualSyncSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.VisualOcclusionMode; +import java.util.Objects; +import javax.annotation.Nonnull; +import lombok.Getter; + +/** + * Authored visual synchronization policy for one PhysicsStore space entity. + */ +public final class VisualSyncSettingsComponent implements Component { + + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.builder( + VisualSyncSettingsComponent.class, + VisualSyncSettingsComponent::new) + .append(new KeyedCodec<>("VisualFullSyncRadius", Codec.INTEGER, false), + (component, value) -> component.visualFullSyncRadius = value != null + ? value + : PhysicsVisualSyncSettings.DEFAULT_VISUAL_FULL_SYNC_RADIUS, + VisualSyncSettingsComponent::getVisualFullSyncRadius) + .add() + .append(new KeyedCodec<>("VisualMaxSyncRadius", Codec.INTEGER, false), + (component, value) -> component.visualMaxSyncRadius = value != null + ? value + : PhysicsVisualSyncSettings.DEFAULT_VISUAL_MAX_SYNC_RADIUS, + VisualSyncSettingsComponent::getVisualMaxSyncRadius) + .add() + .append(new KeyedCodec<>("VisualFarSyncCutoffEnabled", Codec.BOOLEAN, false), + (component, value) -> component.visualFarSyncCutoffEnabled = value != null + ? value + : PhysicsVisualSyncSettings.DEFAULT_VISUAL_FAR_SYNC_CUTOFF_ENABLED, + VisualSyncSettingsComponent::isVisualFarSyncCutoffEnabled) + .add() + .append(new KeyedCodec<>("VisualMidSyncIntervalTicks", Codec.INTEGER, false), + (component, value) -> component.visualMidSyncIntervalTicks = value != null + ? value + : PhysicsVisualSyncSettings.DEFAULT_VISUAL_MID_SYNC_INTERVAL_TICKS, + VisualSyncSettingsComponent::getVisualMidSyncIntervalTicks) + .add() + .append(new KeyedCodec<>("VisualFarSyncIntervalTicks", Codec.INTEGER, false), + (component, value) -> component.visualFarSyncIntervalTicks = value != null + ? value + : PhysicsVisualSyncSettings.DEFAULT_VISUAL_FAR_SYNC_INTERVAL_TICKS, + VisualSyncSettingsComponent::getVisualFarSyncIntervalTicks) + .add() + .append(new KeyedCodec<>("VisualOcclusionMode", + new EnumCodec<>(VisualOcclusionMode.class), + false), + (component, value) -> component.visualOcclusionMode = value != null + ? value + : PhysicsVisualSyncSettings.DEFAULT_VISUAL_OCCLUSION_MODE, + VisualSyncSettingsComponent::getVisualOcclusionMode) + .add() + .append(new KeyedCodec<>("VisualOcclusionRaycastsPerTick", Codec.INTEGER, false), + (component, value) -> component.visualOcclusionRaycastsPerTick = value != null + ? value + : PhysicsVisualSyncSettings.DEFAULT_VISUAL_OCCLUSION_RAYCASTS_PER_TICK, + VisualSyncSettingsComponent::getVisualOcclusionRaycastsPerTick) + .add() + .append(new KeyedCodec<>("VisualOcclusionCacheTicks", Codec.INTEGER, false), + (component, value) -> component.visualOcclusionCacheTicks = value != null + ? value + : PhysicsVisualSyncSettings.DEFAULT_VISUAL_OCCLUSION_CACHE_TICKS, + VisualSyncSettingsComponent::getVisualOcclusionCacheTicks) + .add() + .append(new KeyedCodec<>("VisualSnapshotPredictionEnabled", Codec.BOOLEAN, false), + (component, value) -> component.visualSnapshotPredictionEnabled = value != null + ? value + : PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_PREDICTION_ENABLED, + VisualSyncSettingsComponent::isVisualSnapshotPredictionEnabled) + .add() + .append(new KeyedCodec<>("VisualSnapshotPredictionMaxSeconds", Codec.FLOAT, false), + (component, value) -> component.visualSnapshotPredictionMaxSeconds = value != null + ? value + : PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_PREDICTION_MAX_SECONDS, + VisualSyncSettingsComponent::getVisualSnapshotPredictionMaxSeconds) + .add() + .append(new KeyedCodec<>("VisualSnapshotSmoothingEnabled", Codec.BOOLEAN, false), + (component, value) -> component.visualSnapshotSmoothingEnabled = value != null + ? value + : PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_SMOOTHING_ENABLED, + VisualSyncSettingsComponent::isVisualSnapshotSmoothingEnabled) + .add() + .append(new KeyedCodec<>("VisualSnapshotSmoothingRate", Codec.FLOAT, false), + (component, value) -> component.visualSnapshotSmoothingRate = value != null + ? value + : PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_SMOOTHING_RATE, + VisualSyncSettingsComponent::getVisualSnapshotSmoothingRate) + .add() + .append(new KeyedCodec<>("EntityVisualSyncCullingEnabled", Codec.BOOLEAN, false), + (component, value) -> component.entityVisualSyncCullingEnabled = value != null + ? value + : PhysicsVisualSyncSettings.DEFAULT_ENTITY_VISUAL_SYNC_CULLING_ENABLED, + VisualSyncSettingsComponent::isEntityVisualSyncCullingEnabled) + .add() + .append(new KeyedCodec<>("VisualVisibilityCullingEnabled", Codec.BOOLEAN, false), + (component, value) -> component.visualVisibilityCullingEnabled = value != null + ? value + : PhysicsVisualSyncSettings.DEFAULT_VISUAL_VISIBILITY_CULLING_ENABLED, + VisualSyncSettingsComponent::isVisualVisibilityCullingEnabled) + .add() + .build(); + + @Getter + private int visualFullSyncRadius = + PhysicsVisualSyncSettings.DEFAULT_VISUAL_FULL_SYNC_RADIUS; + @Getter + private int visualMaxSyncRadius = + PhysicsVisualSyncSettings.DEFAULT_VISUAL_MAX_SYNC_RADIUS; + @Getter + private boolean visualFarSyncCutoffEnabled = + PhysicsVisualSyncSettings.DEFAULT_VISUAL_FAR_SYNC_CUTOFF_ENABLED; + @Getter + private int visualMidSyncIntervalTicks = + PhysicsVisualSyncSettings.DEFAULT_VISUAL_MID_SYNC_INTERVAL_TICKS; + @Getter + private int visualFarSyncIntervalTicks = + PhysicsVisualSyncSettings.DEFAULT_VISUAL_FAR_SYNC_INTERVAL_TICKS; + @Nonnull + private VisualOcclusionMode visualOcclusionMode = + PhysicsVisualSyncSettings.DEFAULT_VISUAL_OCCLUSION_MODE; + @Getter + private int visualOcclusionRaycastsPerTick = + PhysicsVisualSyncSettings.DEFAULT_VISUAL_OCCLUSION_RAYCASTS_PER_TICK; + @Getter + private int visualOcclusionCacheTicks = + PhysicsVisualSyncSettings.DEFAULT_VISUAL_OCCLUSION_CACHE_TICKS; + @Getter + private boolean visualSnapshotPredictionEnabled = + PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_PREDICTION_ENABLED; + @Getter + private float visualSnapshotPredictionMaxSeconds = + PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_PREDICTION_MAX_SECONDS; + @Getter + private boolean visualSnapshotSmoothingEnabled = + PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_SMOOTHING_ENABLED; + @Getter + private float visualSnapshotSmoothingRate = + PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_SMOOTHING_RATE; + @Getter + private boolean entityVisualSyncCullingEnabled = + PhysicsVisualSyncSettings.DEFAULT_ENTITY_VISUAL_SYNC_CULLING_ENABLED; + @Getter + private boolean visualVisibilityCullingEnabled = + PhysicsVisualSyncSettings.DEFAULT_VISUAL_VISIBILITY_CULLING_ENABLED; + + public VisualSyncSettingsComponent() { + } + + public VisualSyncSettingsComponent(@Nonnull PhysicsVisualSyncSettings settings) { + visualFullSyncRadius = settings.getVisualFullSyncRadius(); + visualMaxSyncRadius = settings.getVisualMaxSyncRadius(); + visualFarSyncCutoffEnabled = settings.isVisualFarSyncCutoffEnabled(); + visualMidSyncIntervalTicks = settings.getVisualMidSyncIntervalTicks(); + visualFarSyncIntervalTicks = settings.getVisualFarSyncIntervalTicks(); + visualOcclusionMode = settings.getVisualOcclusionMode(); + visualOcclusionRaycastsPerTick = settings.getVisualOcclusionRaycastsPerTick(); + visualOcclusionCacheTicks = settings.getVisualOcclusionCacheTicks(); + visualSnapshotPredictionEnabled = settings.isVisualSnapshotPredictionEnabled(); + visualSnapshotPredictionMaxSeconds = settings.getVisualSnapshotPredictionMaxSeconds(); + visualSnapshotSmoothingEnabled = settings.isVisualSnapshotSmoothingEnabled(); + visualSnapshotSmoothingRate = settings.getVisualSnapshotSmoothingRate(); + entityVisualSyncCullingEnabled = settings.isEntityVisualSyncCullingEnabled(); + visualVisibilityCullingEnabled = settings.isVisualVisibilityCullingEnabled(); + } + + private VisualSyncSettingsComponent(int visualFullSyncRadius, + int visualMaxSyncRadius, + boolean visualFarSyncCutoffEnabled, + int visualMidSyncIntervalTicks, + int visualFarSyncIntervalTicks, + @Nonnull VisualOcclusionMode visualOcclusionMode, + int visualOcclusionRaycastsPerTick, + int visualOcclusionCacheTicks, + boolean visualSnapshotPredictionEnabled, + float visualSnapshotPredictionMaxSeconds, + boolean visualSnapshotSmoothingEnabled, + float visualSnapshotSmoothingRate, + boolean entityVisualSyncCullingEnabled, + boolean visualVisibilityCullingEnabled) { + this.visualFullSyncRadius = visualFullSyncRadius; + this.visualMaxSyncRadius = visualMaxSyncRadius; + this.visualFarSyncCutoffEnabled = visualFarSyncCutoffEnabled; + this.visualMidSyncIntervalTicks = visualMidSyncIntervalTicks; + this.visualFarSyncIntervalTicks = visualFarSyncIntervalTicks; + this.visualOcclusionMode = Objects.requireNonNull(visualOcclusionMode, + "visualOcclusionMode"); + this.visualOcclusionRaycastsPerTick = visualOcclusionRaycastsPerTick; + this.visualOcclusionCacheTicks = visualOcclusionCacheTicks; + this.visualSnapshotPredictionEnabled = visualSnapshotPredictionEnabled; + this.visualSnapshotPredictionMaxSeconds = visualSnapshotPredictionMaxSeconds; + this.visualSnapshotSmoothingEnabled = visualSnapshotSmoothingEnabled; + this.visualSnapshotSmoothingRate = visualSnapshotSmoothingRate; + this.entityVisualSyncCullingEnabled = entityVisualSyncCullingEnabled; + this.visualVisibilityCullingEnabled = visualVisibilityCullingEnabled; + } + + @Nonnull + public VisualOcclusionMode getVisualOcclusionMode() { + return visualOcclusionMode; + } + + public void copyTo(@Nonnull PhysicsVisualSyncSettings target) { + target.setVisualSyncRadii(visualFullSyncRadius, visualMaxSyncRadius); + target.setVisualFarSyncCutoffEnabled(visualFarSyncCutoffEnabled); + target.setVisualMidSyncIntervalTicks(visualMidSyncIntervalTicks); + target.setVisualFarSyncIntervalTicks(visualFarSyncIntervalTicks); + target.setVisualOcclusionMode(visualOcclusionMode); + target.setVisualOcclusionRaycastsPerTick(visualOcclusionRaycastsPerTick); + target.setVisualOcclusionCacheTicks(visualOcclusionCacheTicks); + target.setVisualSnapshotPredictionEnabled(visualSnapshotPredictionEnabled); + target.setVisualSnapshotPredictionMaxSeconds(visualSnapshotPredictionMaxSeconds); + target.setVisualSnapshotSmoothingEnabled(visualSnapshotSmoothingEnabled); + target.setVisualSnapshotSmoothingRate(visualSnapshotSmoothingRate); + target.setEntityVisualSyncCullingEnabled(entityVisualSyncCullingEnabled); + target.setVisualVisibilityCullingEnabled(visualVisibilityCullingEnabled); + } + + public boolean isDefault() { + return visualFullSyncRadius == PhysicsVisualSyncSettings.DEFAULT_VISUAL_FULL_SYNC_RADIUS + && visualMaxSyncRadius == PhysicsVisualSyncSettings.DEFAULT_VISUAL_MAX_SYNC_RADIUS + && visualFarSyncCutoffEnabled + == PhysicsVisualSyncSettings.DEFAULT_VISUAL_FAR_SYNC_CUTOFF_ENABLED + && visualMidSyncIntervalTicks + == PhysicsVisualSyncSettings.DEFAULT_VISUAL_MID_SYNC_INTERVAL_TICKS + && visualFarSyncIntervalTicks + == PhysicsVisualSyncSettings.DEFAULT_VISUAL_FAR_SYNC_INTERVAL_TICKS + && visualOcclusionMode == PhysicsVisualSyncSettings.DEFAULT_VISUAL_OCCLUSION_MODE + && visualOcclusionRaycastsPerTick + == PhysicsVisualSyncSettings.DEFAULT_VISUAL_OCCLUSION_RAYCASTS_PER_TICK + && visualOcclusionCacheTicks + == PhysicsVisualSyncSettings.DEFAULT_VISUAL_OCCLUSION_CACHE_TICKS + && visualSnapshotPredictionEnabled + == PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_PREDICTION_ENABLED + && Float.compare(visualSnapshotPredictionMaxSeconds, + PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_PREDICTION_MAX_SECONDS) == 0 + && visualSnapshotSmoothingEnabled + == PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_SMOOTHING_ENABLED + && Float.compare(visualSnapshotSmoothingRate, + PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_SMOOTHING_RATE) == 0 + && entityVisualSyncCullingEnabled + == PhysicsVisualSyncSettings.DEFAULT_ENTITY_VISUAL_SYNC_CULLING_ENABLED + && visualVisibilityCullingEnabled + == PhysicsVisualSyncSettings.DEFAULT_VISUAL_VISIBILITY_CULLING_ENABLED; + } + + @Nonnull + public static ComponentType getComponentType() { + return PhysicsComponentTypes.visualSyncSettingsComponentType(); + } + + @Nonnull + @Override + public VisualSyncSettingsComponent clone() { + return new VisualSyncSettingsComponent(visualFullSyncRadius, + visualMaxSyncRadius, + visualFarSyncCutoffEnabled, + visualMidSyncIntervalTicks, + visualFarSyncIntervalTicks, + visualOcclusionMode, + visualOcclusionRaycastsPerTick, + visualOcclusionCacheTicks, + visualSnapshotPredictionEnabled, + visualSnapshotPredictionMaxSeconds, + visualSnapshotSmoothingEnabled, + visualSnapshotSmoothingRate, + entityVisualSyncCullingEnabled, + visualVisibilityCullingEnabled); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/package-info.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/package-info.java new file mode 100644 index 00000000..7667ed12 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/package-info.java @@ -0,0 +1,7 @@ +/** + * Public API for the bundled PhysicsEntity module. + *

+ * PhysicsEntity module handles the EntityStore projection of the PhysicsStore ecs entities. + *

+ */ +package dev.hytalemodding.impulse.core.plugin.modules.physicsentity; diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/settings/PhysicsEntitySettingsValidation.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/settings/PhysicsEntitySettingsValidation.java new file mode 100644 index 00000000..f714c495 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/settings/PhysicsEntitySettingsValidation.java @@ -0,0 +1,16 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings; + +import javax.annotation.Nonnull; + +final class PhysicsEntitySettingsValidation { + + private PhysicsEntitySettingsValidation() { + } + + static int requirePositiveAtMost(@Nonnull String label, int value, int maxValue) { + if (value < 1 || value > maxValue) { + throw new IllegalArgumentException(label + " must be between 1 and " + maxValue); + } + return value; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsVisualMaterializationSettings.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/settings/PhysicsVisualMaterializationSettings.java similarity index 91% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsVisualMaterializationSettings.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/settings/PhysicsVisualMaterializationSettings.java index 9c1407c7..66269cb1 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsVisualMaterializationSettings.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/settings/PhysicsVisualMaterializationSettings.java @@ -1,8 +1,8 @@ -package dev.hytalemodding.impulse.core.plugin.settings; +package dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings; +import javax.annotation.Nonnull; import lombok.Getter; import lombok.Setter; -import javax.annotation.Nonnull; /** * Generated visual proxy settings for detached physics bodies. @@ -139,10 +139,11 @@ public class PhysicsVisualMaterializationSettings { DEFAULT_DETACHED_VISUAL_VISIBILITY_CHECK_INTERVAL_TICKS; /** - * Hytale block type used for default detached visual proxies. - *

- * FIXME: this is temporary we cannot assume a specific blocktype since a physics body could be - * composed by any general mix of blocks and entities + * Fallback Hytale block type for generated detached visual proxies. + * + *

This is only a default materialization hint for simple demos and stress tests. Integrators + * that need body-specific visuals should store their own visual description on the physics body + * or disable generated visual materialization.

*/ @Nonnull private String detachedVisualBlockType = DEFAULT_DETACHED_VISUAL_BLOCK_TYPE; @@ -169,7 +170,7 @@ public PhysicsVisualMaterializationSettings( public void setDetachedVisualMaterializationRadius( int detachedVisualMaterializationRadius) { int boundedDetachedVisualMaterializationRadius = - PhysicsSettingsValidation.requirePositiveAtMost( + PhysicsEntitySettingsValidation.requirePositiveAtMost( "Detached visual materialization radius", detachedVisualMaterializationRadius, MAX_DETACHED_VISUAL_MATERIALIZATION_RADIUS); @@ -183,7 +184,7 @@ public void setDetachedVisualMaterializationRadius( public void setDetachedVisualDematerializationRadius( int detachedVisualDematerializationRadius) { int boundedDetachedVisualDematerializationRadius = - PhysicsSettingsValidation.requirePositiveAtMost( + PhysicsEntitySettingsValidation.requirePositiveAtMost( "Detached visual dematerialization radius", detachedVisualDematerializationRadius, MAX_DETACHED_VISUAL_DEMATERIALIZATION_RADIUS); @@ -197,12 +198,12 @@ public void setDetachedVisualDematerializationRadius( public void setDetachedVisualRadii(int detachedVisualMaterializationRadius, int detachedVisualDematerializationRadius) { int boundedDetachedVisualMaterializationRadius = - PhysicsSettingsValidation.requirePositiveAtMost( + PhysicsEntitySettingsValidation.requirePositiveAtMost( "Detached visual materialization radius", detachedVisualMaterializationRadius, MAX_DETACHED_VISUAL_MATERIALIZATION_RADIUS); int boundedDetachedVisualDematerializationRadius = - PhysicsSettingsValidation.requirePositiveAtMost( + PhysicsEntitySettingsValidation.requirePositiveAtMost( "Detached visual dematerialization radius", detachedVisualDematerializationRadius, MAX_DETACHED_VISUAL_DEMATERIALIZATION_RADIUS); @@ -217,7 +218,7 @@ public void setDetachedVisualRadii(int detachedVisualMaterializationRadius, public void setDetachedVisualMaxSpawnsPerTick(int detachedVisualMaxSpawnsPerTick) { this.detachedVisualMaxSpawnsPerTick = - PhysicsSettingsValidation.requirePositiveAtMost( + PhysicsEntitySettingsValidation.requirePositiveAtMost( "Detached visual max spawns per tick", detachedVisualMaxSpawnsPerTick, MAX_DETACHED_VISUAL_MAX_SPAWNS_PER_TICK); @@ -225,7 +226,7 @@ public void setDetachedVisualMaxSpawnsPerTick(int detachedVisualMaxSpawnsPerTick public void setDetachedVisualMaxMaterialized(int detachedVisualMaxMaterialized) { this.detachedVisualMaxMaterialized = - PhysicsSettingsValidation.requirePositiveAtMost( + PhysicsEntitySettingsValidation.requirePositiveAtMost( "Detached visual max materialized", detachedVisualMaxMaterialized, MAX_DETACHED_VISUAL_MAX_MATERIALIZED); @@ -234,7 +235,7 @@ public void setDetachedVisualMaxMaterialized(int detachedVisualMaxMaterialized) public void setDetachedVisualInterestRefreshIntervalTicks( int detachedVisualInterestRefreshIntervalTicks) { this.detachedVisualInterestRefreshIntervalTicks = - PhysicsSettingsValidation.requirePositiveAtMost( + PhysicsEntitySettingsValidation.requirePositiveAtMost( "Detached visual interest refresh interval", detachedVisualInterestRefreshIntervalTicks, MAX_DETACHED_VISUAL_CACHE_INTERVAL_TICKS); @@ -243,7 +244,7 @@ public void setDetachedVisualInterestRefreshIntervalTicks( public void setDetachedVisualCandidateRefreshIntervalTicks( int detachedVisualCandidateRefreshIntervalTicks) { this.detachedVisualCandidateRefreshIntervalTicks = - PhysicsSettingsValidation.requirePositiveAtMost( + PhysicsEntitySettingsValidation.requirePositiveAtMost( "Detached visual candidate refresh interval", detachedVisualCandidateRefreshIntervalTicks, MAX_DETACHED_VISUAL_CACHE_INTERVAL_TICKS); @@ -252,7 +253,7 @@ public void setDetachedVisualCandidateRefreshIntervalTicks( public void setDetachedVisualVisibilityCheckIntervalTicks( int detachedVisualVisibilityCheckIntervalTicks) { this.detachedVisualVisibilityCheckIntervalTicks = - PhysicsSettingsValidation.requirePositiveAtMost( + PhysicsEntitySettingsValidation.requirePositiveAtMost( "Detached visual visibility check interval", detachedVisualVisibilityCheckIntervalTicks, MAX_DETACHED_VISUAL_CACHE_INTERVAL_TICKS); diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsVisualSyncSettings.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/settings/PhysicsVisualSyncSettings.java similarity index 91% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsVisualSyncSettings.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/settings/PhysicsVisualSyncSettings.java index d9f3fc6b..10cc542e 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsVisualSyncSettings.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/settings/PhysicsVisualSyncSettings.java @@ -1,8 +1,9 @@ -package dev.hytalemodding.impulse.core.plugin.settings; +package dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.VisualOcclusionMode; +import javax.annotation.Nonnull; import lombok.Getter; import lombok.Setter; -import javax.annotation.Nonnull; /** * Entity and follower transform sync sampling settings for a physics space. @@ -114,7 +115,7 @@ public class PhysicsVisualSyncSettings { public static final float MAX_VISUAL_SNAPSHOT_SMOOTHING_RATE = 120.0f; /** - * Whether owner entity transforms should use player-interest culling. + * Whether body-attached entity transforms should use player-interest culling. * Disabled by default because gameplay code may rely on server-side transforms even * when no player is currently near the body. */ @@ -209,7 +210,7 @@ public class PhysicsVisualSyncSettings { /** * If enabled, entity-backed physics body transforms use the same player-interest culling - * as follower visuals. Controlled bodies are always synced. + * as follower visuals. Kinematic bodies are always synced. */ @Setter @Getter @@ -244,7 +245,7 @@ public PhysicsVisualSyncSettings(@Nonnull PhysicsVisualSyncSettings settings) { } public void setVisualFullSyncRadius(int visualFullSyncRadius) { - int boundedVisualFullSyncRadius = PhysicsSettingsValidation.requirePositiveAtMost( + int boundedVisualFullSyncRadius = PhysicsEntitySettingsValidation.requirePositiveAtMost( "Visual full sync radius", visualFullSyncRadius, MAX_VISUAL_FULL_SYNC_RADIUS); @@ -256,7 +257,7 @@ public void setVisualFullSyncRadius(int visualFullSyncRadius) { } public void setVisualMaxSyncRadius(int visualMaxSyncRadius) { - int boundedVisualMaxSyncRadius = PhysicsSettingsValidation.requirePositiveAtMost( + int boundedVisualMaxSyncRadius = PhysicsEntitySettingsValidation.requirePositiveAtMost( "Visual max sync radius", visualMaxSyncRadius, MAX_VISUAL_MAX_SYNC_RADIUS); @@ -268,11 +269,11 @@ public void setVisualMaxSyncRadius(int visualMaxSyncRadius) { } public void setVisualSyncRadii(int visualFullSyncRadius, int visualMaxSyncRadius) { - int boundedVisualFullSyncRadius = PhysicsSettingsValidation.requirePositiveAtMost( + int boundedVisualFullSyncRadius = PhysicsEntitySettingsValidation.requirePositiveAtMost( "Visual full sync radius", visualFullSyncRadius, MAX_VISUAL_FULL_SYNC_RADIUS); - int boundedVisualMaxSyncRadius = PhysicsSettingsValidation.requirePositiveAtMost( + int boundedVisualMaxSyncRadius = PhysicsEntitySettingsValidation.requirePositiveAtMost( "Visual max sync radius", visualMaxSyncRadius, MAX_VISUAL_MAX_SYNC_RADIUS); @@ -285,14 +286,14 @@ public void setVisualSyncRadii(int visualFullSyncRadius, int visualMaxSyncRadius } public void setVisualMidSyncIntervalTicks(int visualMidSyncIntervalTicks) { - this.visualMidSyncIntervalTicks = PhysicsSettingsValidation.requirePositiveAtMost( + this.visualMidSyncIntervalTicks = PhysicsEntitySettingsValidation.requirePositiveAtMost( "Visual mid sync interval", visualMidSyncIntervalTicks, MAX_VISUAL_MID_SYNC_INTERVAL_TICKS); } public void setVisualFarSyncIntervalTicks(int visualFarSyncIntervalTicks) { - this.visualFarSyncIntervalTicks = PhysicsSettingsValidation.requirePositiveAtMost( + this.visualFarSyncIntervalTicks = PhysicsEntitySettingsValidation.requirePositiveAtMost( "Visual far sync interval", visualFarSyncIntervalTicks, MAX_VISUAL_FAR_SYNC_INTERVAL_TICKS); @@ -308,14 +309,14 @@ public void setVisualOcclusionMode(@Nonnull VisualOcclusionMode visualOcclusionM } public void setVisualOcclusionRaycastsPerTick(int visualOcclusionRaycastsPerTick) { - this.visualOcclusionRaycastsPerTick = PhysicsSettingsValidation.requirePositiveAtMost( + this.visualOcclusionRaycastsPerTick = PhysicsEntitySettingsValidation.requirePositiveAtMost( "Visual occlusion raycasts per tick", visualOcclusionRaycastsPerTick, MAX_VISUAL_OCCLUSION_RAYCASTS_PER_TICK); } public void setVisualOcclusionCacheTicks(int visualOcclusionCacheTicks) { - this.visualOcclusionCacheTicks = PhysicsSettingsValidation.requirePositiveAtMost( + this.visualOcclusionCacheTicks = PhysicsEntitySettingsValidation.requirePositiveAtMost( "Visual occlusion cache ticks", visualOcclusionCacheTicks, MAX_VISUAL_OCCLUSION_CACHE_TICKS); diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/VisualOcclusionMode.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/settings/VisualOcclusionMode.java similarity index 67% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/VisualOcclusionMode.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/settings/VisualOcclusionMode.java index 34feef2c..8ed13a20 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/VisualOcclusionMode.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/settings/VisualOcclusionMode.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.plugin.settings; +package dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings; public enum VisualOcclusionMode { diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/worldcollision/ImpulseWorldCollisionPlugin.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/worldcollision/ImpulseWorldCollisionPlugin.java deleted file mode 100644 index e325c13d..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/worldcollision/ImpulseWorldCollisionPlugin.java +++ /dev/null @@ -1,44 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.modules.worldcollision; - -import com.hypixel.hytale.component.ComponentRegistryProxy; -import com.hypixel.hytale.server.core.plugin.JavaPlugin; -import com.hypixel.hytale.server.core.plugin.JavaPluginInit; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldCollisionLifecycle; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.commands.WorldCollisionCommandContributions; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems.PhysicsChunkBoundarySystem; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems.PhysicsCollisionLodSystem; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems.PhysicsWorldCollisionStreamingSystem; -import javax.annotation.Nonnull; - -/** - * Subplugin that enables Impulse world collision. - */ -public final class ImpulseWorldCollisionPlugin extends JavaPlugin { - - public ImpulseWorldCollisionPlugin(@Nonnull JavaPluginInit init) { - super(init); - } - - @Override - protected void setup() { - ComponentRegistryProxy entityRegistry = getEntityStoreRegistry(); - WorldCollisionProfilingResource.setResourceType(entityRegistry.registerResource( - WorldCollisionProfilingResource.class, - WorldCollisionProfilingResource::new)); - entityRegistry.registerSystem(new PhysicsWorldCollisionStreamingSystem()); - entityRegistry.registerSystem(new PhysicsCollisionLodSystem()); - entityRegistry.registerSystem(new PhysicsChunkBoundarySystem()); - - WorldCollisionCommandContributions.register(); - WorldCollisionLifecycle.enable(); - } - - @Override - protected void shutdown() { - WorldCollisionLifecycle.disable(); - WorldCollisionCommandContributions.unregister(); - WorldCollisionProfilingResource.clearResourceType(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/worldcollision/WorldCollisionBuildStats.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/worldcollision/WorldCollisionBuildStats.java deleted file mode 100644 index b6821cc3..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/worldcollision/WorldCollisionBuildStats.java +++ /dev/null @@ -1,16 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.modules.worldcollision; - -/** - * Aggregate statistics from building or rebuilding streamed world-collision geometry. - */ -public record WorldCollisionBuildStats(int scannedBlocks, - int solidBlocks, - int culledInteriorBlocks, - int fullCubeRuns, - int detailBoxes, - int colliderBodies, - int removedBodies, - int sectionsBuilt, - int sectionsRebuilt, - int voxelBodies) { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/worldcollision/WorldCollisionMode.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/worldcollision/WorldCollisionMode.java deleted file mode 100644 index 06961cd1..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/worldcollision/WorldCollisionMode.java +++ /dev/null @@ -1,21 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.modules.worldcollision; - -/** - * Controls how a physics space interacts with Hytale world voxel collision. - * - *

This is an opt-in per-space policy. Impulse does not impose world collision - * on any space by default; the integrator chooses the level of intrusion.

- * - *
    - *
  • {@link #NONE} - No world collision. The space is pure physics with no terrain.
  • - *
  • {@link #MANUAL} - World collision exists but must be built/cleared explicitly - * by the integrator (e.g. via commands or a custom system).
  • - *
  • {@link #STREAMING} - Impulse automatically streams section collision around - * tracked players/bodies and prunes unused sections after a TTL.
  • - *
- */ -public enum WorldCollisionMode { - NONE, - MANUAL, - STREAMING -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/worldcollision/WorldCollisionPrewarmStats.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/worldcollision/WorldCollisionPrewarmStats.java deleted file mode 100644 index bca78885..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/worldcollision/WorldCollisionPrewarmStats.java +++ /dev/null @@ -1,8 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.modules.worldcollision; - -/** - * Statistics from ensuring world collision around multiple target positions. - */ -public record WorldCollisionPrewarmStats(int sectionTargets, - WorldCollisionBuildStats buildStats) { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/worldcollision/WorldCollisionStats.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/worldcollision/WorldCollisionStats.java deleted file mode 100644 index a107c609..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/modules/worldcollision/WorldCollisionStats.java +++ /dev/null @@ -1,10 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.modules.worldcollision; - -/** - * Current size of the generated world-collision cache. - */ -public record WorldCollisionStats(int spaces, - int sections, - int bodies, - int shapeTemplates) { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/package-info.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/package-info.java index 8edbc8a1..add06457 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/package-info.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/package-info.java @@ -3,6 +3,6 @@ * *

Types under this package tree are the preferred import surface for * third-party Hytale plugins. Backend-neutral physics contracts remain in the - * {@code impulse-api} module.

+ * {@code impulse-backend-api} artifact.

*/ package dev.hytalemodding.impulse.core.plugin; diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/persistence/PhysicsPersistence.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/persistence/PhysicsPersistence.java index 0ad4d4e0..bc37513f 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/persistence/PhysicsPersistence.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/persistence/PhysicsPersistence.java @@ -2,9 +2,16 @@ import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RuntimeJointCountQuery; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.persistence.PhysicsStoreHolderStorage; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsDiagnostics; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.physics.SpaceSummary; +import java.util.List; +import java.util.concurrent.CompletionStage; import javax.annotation.Nonnull; /** @@ -13,94 +20,149 @@ public final class PhysicsPersistence { public static final int CURRENT_SCHEMA_VERSION = - PhysicsPersistenceResource.CURRENT_SCHEMA_VERSION; + PhysicsStoreHolderStorage.SCHEMA_VERSION; + private static final String SAVE_SKIPPED_REASON = + "authoritative-physics-store-holder-save-hook"; + private static final String RESTORE_SKIPPED_REASON = + "authoritative-physics-store-auto-restore"; private PhysicsPersistence() { } @Nonnull public static SaveResult saveRuntimeSnapshot(@Nonnull Store store) { - PhysicsPersistenceResource persistent = persistent(store); - PhysicsWorldResource runtime = runtime(store); - PhysicsPersistenceSyncResult result = persistent.saveRuntimeSnapshot(store, runtime); - return new SaveResult(result.synced(), - persistent.getSchemaVersion(), - result.spaces(), - result.bodies(), - result.joints(), - result.skippedReason()); + Status status = status(store); + return new SaveResult(false, + status.schemaVersion(), + status.storedSpaces(), + status.storedBodies(), + status.storedJoints(), + SAVE_SKIPPED_REASON); + } + + @Nonnull + public static CompletionStage saveRuntimeSnapshotAsync( + @Nonnull Store store) { + return statusAsync(store).thenApply(status -> new SaveResult(false, + status.schemaVersion(), + status.storedSpaces(), + status.storedBodies(), + status.storedJoints(), + SAVE_SKIPPED_REASON)); } @Nonnull public static RestoreRequestResult requestRuntimeRestore(@Nonnull Store store) { - PhysicsPersistenceResource persistent = persistent(store); Status status = status(store); - if (persistent.isRuntimeRestorePending()) { - return new RestoreRequestResult(false, "already-pending", status); - } - if (persistent.getSchemaVersion() != CURRENT_SCHEMA_VERSION) { - return new RestoreRequestResult(false, "schema-mismatch", status); - } + return new RestoreRequestResult(false, RESTORE_SKIPPED_REASON, status); + } - persistent.markRuntimeRestorePending(); - return new RestoreRequestResult(true, "", status(store)); + @Nonnull + public static CompletionStage requestRuntimeRestoreAsync( + @Nonnull Store store) { + return statusAsync(store).thenApply(status -> + new RestoreRequestResult(false, RESTORE_SKIPPED_REASON, status)); } @Nonnull public static Status status(@Nonnull Store store) { - PhysicsPersistenceResource persistent = persistent(store); - PhysicsWorldResource runtime = runtime(store); - return new Status(runtime.getSpaceCount(), - runtime.getBodyRegistrationCount(PhysicsBodyPersistenceMode.PERSISTENT), - runtime.getBodyRegistrationCount(PhysicsBodyPersistenceMode.RUNTIME_ONLY), - countRuntimeJoints(runtime), - persistent.getSchemaVersion(), - persistent.getSpaceCount(), - persistent.getBodyCount(), - persistent.getJointCount(), - restoreState(persistent), - restoreMessage(persistent)); - } - - private static int countRuntimeJoints(@Nonnull PhysicsWorldResource runtime) { - return runtime.query(new RuntimeJointCountQuery()) - .completion() - .toCompletableFuture() - .join(); + Store physicsStore = physicsStore(store); + return copiedStatus(physicsStore); } @Nonnull - private static RestoreState restoreState(@Nonnull PhysicsPersistenceResource persistent) { - if (persistent.hasRuntimeRestoreFailed()) { - return RestoreState.FAILED; - } - if (!persistent.isRuntimeRestorePending()) { - return RestoreState.IDLE; - } - return persistent.isRuntimeSpaceBootstrapComplete() - ? RestoreState.PENDING_BODIES_AND_JOINTS - : RestoreState.PENDING_SPACES; + public static CompletionStage statusAsync(@Nonnull Store store) { + return PhysicsThreading.enqueueReadOnWorldThread(store.getExternalData().getWorld(), + "queue PhysicsStore persistence status read", + PhysicsPersistence::liveStatus); } @Nonnull - private static String restoreMessage(@Nonnull PhysicsPersistenceResource persistent) { - if (persistent.hasRuntimeRestoreFailed()) { - return persistent.runtimeRestoreFailureSummary(); - } - if (persistent.hasRuntimeRestoreSkips()) { - return persistent.runtimeRestoreSummary(); + private static Store physicsStore(@Nonnull Store store) { + return PhysicsThreading.store(store.getExternalData().getWorld()); + } + + @Nonnull + private static Status liveStatus(@Nonnull Store physicsStore) { + SavedStateSummary saved = savedStateSummary(physicsStore); + PhysicsRestoreStatusResource restore = physicsStore.getResource( + PhysicsRestoreStatusResource.getResourceType()); + List summaries = PhysicsDiagnostics.spaceSummaries(physicsStore); + int runtimeBodies = summaries.stream().mapToInt(SpaceSummary::bodyCount).sum(); + int runtimeJoints = summaries.stream().mapToInt(SpaceSummary::jointCount).sum(); + int physicsStoreSpaces = physicsStore.getResource( + PhysicsSpaceCompatibilityIndexResource.getResourceType()).size(); + return new Status(Math.max(physicsStoreSpaces, + summaries.size()), + runtimeBodies, + 0, + runtimeJoints, + saved.schemaVersion(), + saved.spaces(), + saved.bodies(), + saved.joints(), + restoreState(restore), + restoreMessage(restore)); + } + + @Nonnull + private static Status copiedStatus(@Nonnull Store physicsStore) { + PhysicsThreading.requireWorldThread(physicsStore, + "read copied PhysicsStore persistence status"); + SavedStateSummary saved = savedStateSummary(physicsStore); + PhysicsRestoreStatusResource restore = physicsStore.getResource( + PhysicsRestoreStatusResource.getResourceType()); + int runtimeBodies = physicsStore.getResource(PhysicsSnapshotResource.getResourceType()) + .getLatestFrame() + .bodies() + .size(); + int physicsStoreSpaces = physicsStore.getResource( + PhysicsSpaceCompatibilityIndexResource.getResourceType()).size(); + return new Status(physicsStoreSpaces, + runtimeBodies, + 0, + saved.joints(), + saved.schemaVersion(), + saved.spaces(), + saved.bodies(), + saved.joints(), + restoreState(restore), + restoreMessage(restore)); + } + + @Nonnull + private static SavedStateSummary savedStateSummary(@Nonnull Store physicsStore) { + PhysicsStoreHolderStorage.Summary holderSummary = PhysicsStoreHolderStorage.summary( + physicsStore); + if (holderSummary.present()) { + return new SavedStateSummary(CURRENT_SCHEMA_VERSION, + holderSummary.spaces(), + holderSummary.bodies(), + holderSummary.joints()); } - return ""; + return new SavedStateSummary(CURRENT_SCHEMA_VERSION, 0, 0, 0); } @Nonnull - private static PhysicsPersistenceResource persistent(@Nonnull Store store) { - return store.getResource(PhysicsPersistenceResource.getResourceType()); + private static RestoreState restoreState(@Nonnull PhysicsRestoreStatusResource restore) { + if (restore.isFailed()) { + return RestoreState.FAILED; + } + if (restore.isPending()) { + return RestoreState.PENDING_SPACES; + } + return RestoreState.IDLE; } @Nonnull - private static PhysicsWorldResource runtime(@Nonnull Store store) { - return store.getResource(PhysicsWorldResource.getResourceType()); + private static String restoreMessage(@Nonnull PhysicsRestoreStatusResource restore) { + if (restore.isFailed()) { + return restore.getFailureMessage(); + } + if (!restore.getSoftSkipsByReason().isEmpty()) { + return "PhysicsStore restore soft skips: " + restore.getSoftSkipsByReason(); + } + return ""; } public enum RestoreState { @@ -150,4 +212,8 @@ public boolean hasRestoreMessage() { return !restoreMessage.isEmpty(); } } + + private record SavedStateSummary(int schemaVersion, int spaces, int bodies, int joints) { + } + } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/persistence/PhysicsPersistenceResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/persistence/PhysicsPersistenceResource.java deleted file mode 100644 index 87b307c6..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/persistence/PhysicsPersistenceResource.java +++ /dev/null @@ -1,55 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.persistence; - -import com.hypixel.hytale.component.Resource; -import com.hypixel.hytale.component.ResourceType; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import javax.annotation.Nonnull; - -/** - * Plugin-facing contract for Impulse's world-level persistence resource. - */ -public abstract class PhysicsPersistenceResource implements Resource { - - public static final int CURRENT_SCHEMA_VERSION = 6; - - @Nonnull - public static ResourceType getResourceType() { - return ImpulsePlugin.get().getPersistentPhysicsWorldResourceType(); - } - - public abstract int getSchemaVersion(); - - public abstract int getSpaceCount(); - - public abstract int getBodyCount(); - - public abstract int getJointCount(); - - public abstract boolean isRuntimeRestorePending(); - - public abstract void markRuntimeRestorePending(); - - public abstract boolean isRuntimeSpaceBootstrapComplete(); - - public abstract boolean hasRuntimeRestoreFailed(); - - public abstract boolean hasRuntimeRestoreSkips(); - - @Nonnull - public abstract String runtimeRestoreFailureSummary(); - - @Nonnull - public abstract String runtimeRestoreSummary(); - - @Nonnull - public abstract PhysicsPersistenceSyncResult saveRuntimeSnapshot( - @Nonnull Store store, - @Nonnull PhysicsWorldResource runtime); - - @Nonnull - @Override - public abstract PhysicsPersistenceResource clone(); -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/persistence/PhysicsPersistenceSyncResult.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/persistence/PhysicsPersistenceSyncResult.java deleted file mode 100644 index 92f598a8..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/persistence/PhysicsPersistenceSyncResult.java +++ /dev/null @@ -1,10 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.persistence; - -import javax.annotation.Nonnull; - -public record PhysicsPersistenceSyncResult(boolean synced, - int spaces, - int bodies, - int joints, - @Nonnull String skippedReason) { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsAsync.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsAsync.java new file mode 100644 index 00000000..b928600e --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsAsync.java @@ -0,0 +1,62 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import com.hypixel.hytale.server.core.universe.world.World; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import javax.annotation.Nonnull; + +/** + * Helpers for consuming copied PhysicsStore async results from world-thread code. + */ +public final class PhysicsAsync { + + private PhysicsAsync() { + } + + @Nonnull + public static CompletableFuture acceptOnWorldThread(@Nonnull World world, + @Nonnull CompletionStage stage, + @Nonnull Consumer consumer) { + + Objects.requireNonNull(world, "world"); + Objects.requireNonNull(stage, "stage"); + Objects.requireNonNull(consumer, "consumer"); + + CompletableFuture completion = new CompletableFuture<>(); + stage.whenComplete((value, failure) -> { + if (failure != null) { + completion.completeExceptionally(unwrap(failure)); + return; + } + try { + world.execute(() -> accept(value, consumer, completion)); + } catch (RuntimeException exception) { + completion.completeExceptionally(exception); + } + }); + return completion; + } + + private static void accept(T value, + @Nonnull Consumer consumer, + @Nonnull CompletableFuture completion) { + try { + consumer.accept(value); + completion.complete(null); + } catch (RuntimeException | Error exception) { + completion.completeExceptionally(exception); + } + } + + @Nonnull + private static Throwable unwrap(@Nonnull Throwable failure) { + if (failure instanceof CompletionException completionException + && completionException.getCause() != null) { + return completionException.getCause(); + } + return failure; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsBackendAccess.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsBackendAccess.java new file mode 100644 index 00000000..1f86a182 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsBackendAccess.java @@ -0,0 +1,203 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeStatsSink; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource.BodyHitMetadata; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +final class PhysicsBackendAccess { + + private PhysicsBackendAccess() { + } + + @Nullable + static SpaceContext space(@Nonnull Store store, + @Nonnull Ref spaceRef) { + PhysicsThreading.requireBackendIdle(store, "read live PhysicsStore backend state"); + Objects.requireNonNull(spaceRef, "spaceRef"); + if (spaceRef.getStore() != store || !spaceRef.isValid()) { + return null; + } + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + return space(runtime, spaceRef); + } + + @Nullable + static SpaceContext space(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull Ref spaceRef) { + UUID spaceUuid = runtime.getSpaceUuid(spaceRef); + BackendSpaceHandle spaceHandle = runtime.getSpaceHandle(spaceRef); + BackendId backendId = runtime.getSpaceBackendId(spaceRef); + PhysicsBackendRuntime backendRuntime = + backendId != null ? runtime.getRuntime(backendId) : null; + if (spaceUuid == null || spaceHandle == null || backendId == null || backendRuntime == null) { + return null; + } + return new SpaceContext(spaceUuid, backendId, spaceHandle, backendRuntime); + } + + @Nonnull + static SpaceContext requireSpace(@Nonnull Store store, + @Nonnull Ref spaceRef) { + SpaceContext space = space(store, spaceRef); + if (space == null) { + throw new IllegalArgumentException("Physics space ref=" + spaceRef + + " is not registered"); + } + return space; + } + + @Nonnull + static SpaceSummary summary(@Nonnull PhysicsSpaceCompatibilityIndexResource compatibility, + @Nonnull SpaceContext space) { + SpaceId spaceId = compatibility.getSpaceId(space.spaceUuid()); + if (spaceId == null) { + throw new IllegalStateException("PhysicsStore space has no compatibility SpaceId: " + + space.spaceUuid()); + } + RuntimeStatsCapture runtimeStats = new RuntimeStatsCapture(); + space.backendRuntime().runtimeStats(space.spaceHandle().value(), runtimeStats); + return new SpaceSummary(spaceId, + space.backendId(), + space.backendRuntime().bodyCount(space.spaceHandle().value()), + space.backendRuntime().jointCount(space.spaceHandle().value()), + runtimeStats.available(), + runtimeStats.bodyCount(), + runtimeStats.colliderCount(), + runtimeStats.activeBodyCount(), + runtimeStats.contactPairCount(), + runtimeStats.contactManifoldCount(), + runtimeStats.contactPointCount(), + runtimeStats.dynamicDynamicContactPairCount(), + runtimeStats.terrainContactPairCount(), + runtimeStats.activeIslandCount(), + runtimeStats.jointCount()); + } + + @Nonnull + static RaycastHitView toView(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull SpaceContext space, + long bodyId, + float pointX, + float pointY, + float pointZ, + float normalX, + float normalY, + float normalZ, + float fraction, + float distance) { + BodyHitMetadata metadata = runtime.getBodyHitMetadata(space.backendId(), + space.spaceHandle(), + bodyId); + return new RaycastHitView(metadata != null ? metadata.bodyRef() : null, + pointX, + pointY, + pointZ, + normalX, + normalY, + normalZ, + fraction, + distance); + } + + record SpaceContext(@Nonnull UUID spaceUuid, + @Nonnull BackendId backendId, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull PhysicsBackendRuntime backendRuntime) { + } + + private static final class RuntimeStatsCapture implements BackendRuntimeStatsSink { + + private int bodyCount; + private int colliderCount; + private int activeBodyCount; + private int contactPairCount; + private int contactManifoldCount; + private int contactPointCount; + private int dynamicDynamicContactPairCount; + private int terrainContactPairCount; + private int activeIslandCount; + private int jointCount; + private boolean available; + + @Override + public void accept(int bodyCount, + int colliderCount, + int activeBodyCount, + int contactPairCount, + int contactManifoldCount, + int contactPointCount, + int dynamicDynamicContactPairCount, + int terrainContactPairCount, + int activeIslandCount, + int jointCount, + boolean available) { + this.bodyCount = bodyCount; + this.colliderCount = colliderCount; + this.activeBodyCount = activeBodyCount; + this.contactPairCount = contactPairCount; + this.contactManifoldCount = contactManifoldCount; + this.contactPointCount = contactPointCount; + this.dynamicDynamicContactPairCount = dynamicDynamicContactPairCount; + this.terrainContactPairCount = terrainContactPairCount; + this.activeIslandCount = activeIslandCount; + this.jointCount = jointCount; + this.available = available; + } + + private int bodyCount() { + return bodyCount; + } + + private int colliderCount() { + return colliderCount; + } + + private int activeBodyCount() { + return activeBodyCount; + } + + private int contactPairCount() { + return contactPairCount; + } + + private int contactManifoldCount() { + return contactManifoldCount; + } + + private int contactPointCount() { + return contactPointCount; + } + + private int dynamicDynamicContactPairCount() { + return dynamicDynamicContactPairCount; + } + + private int terrainContactPairCount() { + return terrainContactPairCount; + } + + private int activeIslandCount() { + return activeIslandCount; + } + + private int jointCount() { + return jointCount; + } + + private boolean available() { + return available; + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsBodies.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsBodies.java new file mode 100644 index 00000000..36b538be --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsBodies.java @@ -0,0 +1,248 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsTopologyMutations; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.plugin.components.BodyCommandComponent; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsSnapshotFrame; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletionStage; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Public body helpers for PhysicsStore entities. + */ +public final class PhysicsBodies { + + private PhysicsBodies() { + } + + @Nullable + public static SpaceId spaceId(@Nonnull Store store, + @Nonnull UUID bodyUuid) { + Store checkedStore = requireWorldThread(store, + "read copied PhysicsStore body space"); + PhysicsBodySnapshot snapshot = snapshotResource(checkedStore) + .getBody(Objects.requireNonNull(bodyUuid, "bodyUuid")); + return spaceId(checkedStore, snapshot); + } + + @Nullable + public static SpaceId spaceId(@Nonnull Store store, + @Nonnull Ref bodyRef) { + Store checkedStore = requireWorldThread(store, + "read copied PhysicsStore body space"); + Ref checkedRef = Objects.requireNonNull(bodyRef, "bodyRef"); + if (!sameValidStore(checkedStore, checkedRef)) { + return null; + } + return spaceId(checkedStore, snapshotResource(checkedStore).getBody(checkedRef)); + } + + public static boolean isRegistered(@Nonnull Store store, + @Nonnull UUID bodyUuid) { + Store checkedStore = requireWorldThread(store, + "read copied PhysicsStore body registration"); + PhysicsBodySnapshot snapshot = snapshotResource(checkedStore) + .getBody(Objects.requireNonNull(bodyUuid, "bodyUuid")); + return spaceId(checkedStore, snapshot) != null; + } + + public static boolean isRegistered(@Nonnull Store store, + @Nonnull Ref bodyRef) { + Store checkedStore = requireWorldThread(store, + "read copied PhysicsStore body registration"); + Ref checkedRef = Objects.requireNonNull(bodyRef, "bodyRef"); + if (!sameValidStore(checkedStore, checkedRef)) { + return false; + } + return spaceId(checkedStore, snapshotResource(checkedStore).getBody(checkedRef)) != null; + } + + @Nonnull + public static Collection bodyUuids(@Nonnull Store store) { + Store checkedStore = requireWorldThread(store, + "read copied PhysicsStore body registrations"); + Collection bodies = snapshotFrame(checkedStore).bodies(); + ArrayList bodyUuids = new ArrayList<>(bodies.size()); + for (PhysicsBodySnapshot body : bodies) { + if (spaceId(checkedStore, body) != null) { + bodyUuids.add(body.bodyUuid()); + } + } + return bodyUuids; + } + + public static int registrationCount(@Nonnull Store store) { + Store checkedStore = requireWorldThread(store, + "count copied PhysicsStore body registrations"); + int count = 0; + for (PhysicsBodySnapshot body : snapshotFrame(checkedStore).bodies()) { + if (spaceId(checkedStore, body) != null) { + count++; + } + } + return count; + } + + public static int registrationCount(@Nonnull Store store, + @Nonnull SpaceId spaceId) { + Store checkedStore = requireWorldThread(store, + "count copied PhysicsStore body registrations"); + SpaceId checkedSpaceId = Objects.requireNonNull(spaceId, "spaceId"); + int count = 0; + for (PhysicsBodySnapshot body : snapshotFrame(checkedStore).bodies()) { + if (checkedSpaceId.equals(spaceId(checkedStore, body))) { + count++; + } + } + return count; + } + + @Nullable + public static PhysicsBodySnapshot snapshot(@Nonnull Store store, + @Nonnull Ref bodyRef) { + Store checkedStore = requireWorldThread(store, + "read copied PhysicsStore body snapshot"); + Ref checkedRef = Objects.requireNonNull(bodyRef, "bodyRef"); + if (!sameValidStore(checkedStore, checkedRef)) { + return null; + } + return checkedStore.getResource(PhysicsSnapshotResource.getResourceType()) + .getBody(checkedRef); + } + + @Nullable + public static PhysicsBodySnapshot snapshot(@Nonnull Store store, + @Nonnull UUID bodyUuid) { + Store checkedStore = requireWorldThread(store, + "read copied PhysicsStore body snapshot"); + return checkedStore.getResource(PhysicsSnapshotResource.getResourceType()) + .getBody(Objects.requireNonNull(bodyUuid, "bodyUuid")); + } + + @Nonnull + public static PhysicsSnapshotFrame snapshotFrame(@Nonnull Store store) { + Store checkedStore = requireWorldThread(store, + "read copied PhysicsStore body snapshot frame"); + return checkedStore.getResource(PhysicsSnapshotResource.getResourceType()) + .getLatestFrame(); + } + + public static int snapshotCount(@Nonnull Store store) { + return snapshotFrame(store).bodies().size(); + } + + public static void appendCommand(@Nonnull Store store, + @Nonnull Ref bodyRef, + @Nonnull BodyCommandComponent command) { + Store checkedStore = requireWorldThread(store, + "append a PhysicsStore body command"); + Ref checkedRef = requireSameValidStore(checkedStore, + bodyRef, + "bodyRef"); + BodyCommandComponent checkedCommand = Objects.requireNonNull(command, "command"); + BodyCommandComponent existing = checkedStore.getComponent(checkedRef, + BodyCommandComponent.getComponentType()); + BodyCommandComponent merged = existing != null + ? existing.append(checkedCommand) + : checkedCommand.clone(); + checkedStore.putComponent(checkedRef, BodyCommandComponent.getComponentType(), merged); + } + + public static void destroy(@Nonnull Store store, + @Nonnull UUID bodyUuid) { + Store checkedStore = requireWorldThread(store, + "destroy a PhysicsStore body entity"); + PhysicsTopologyMutations.destroyBody(checkedStore, + Objects.requireNonNull(bodyUuid, "bodyUuid")); + } + + public static void destroy(@Nonnull Store store, + @Nonnull Ref bodyRef) { + Store checkedStore = requireWorldThread(store, + "destroy a PhysicsStore body entity"); + Ref checkedRef = requireSameValidStore(checkedStore, + bodyRef, + "bodyRef"); + destroy(checkedStore, PhysicsEntityRefs.entityUuid(checkedRef)); + } + + @Nonnull + public static CompletionStage destroyAsync(@Nonnull World world, + @Nonnull UUID bodyUuid) { + UUID checkedBodyUuid = Objects.requireNonNull(bodyUuid, "bodyUuid"); + return PhysicsThreading.callWhenBackendIdleOnWorldThread(world, + "destroy a PhysicsStore body entity", + store -> { + destroy(store, checkedBodyUuid); + return checkedBodyUuid; + }); + } + + @Nonnull + public static CompletionStage destroyAsync(@Nonnull World world, + @Nonnull Ref bodyRef) { + Ref checkedRef = Objects.requireNonNull(bodyRef, "bodyRef"); + return PhysicsThreading.callWhenBackendIdleOnWorldThread(world, + "destroy a PhysicsStore body entity", + store -> { + Ref sameStoreRef = requireSameValidStore(store, + checkedRef, + "bodyRef"); + UUID bodyUuid = PhysicsEntityRefs.entityUuid(sameStoreRef); + destroy(store, bodyUuid); + return bodyUuid; + }); + } + + @Nonnull + private static Store requireWorldThread(@Nonnull Store store, + @Nonnull String operation) { + Store checkedStore = Objects.requireNonNull(store, "store"); + PhysicsThreading.requireWorldThread(checkedStore, operation); + return checkedStore; + } + + @Nonnull + private static Ref requireSameValidStore(@Nonnull Store store, + @Nonnull Ref ref, + @Nonnull String name) { + Ref checkedRef = Objects.requireNonNull(ref, name); + if (sameValidStore(store, checkedRef)) { + return checkedRef; + } + throw new IllegalArgumentException("PhysicsStore body ref is not valid for this store: " + + name); + } + + private static boolean sameValidStore(@Nonnull Store store, + @Nonnull Ref ref) { + return ref.getStore() == store && ref.isValid(); + } + + @Nonnull + private static PhysicsSnapshotResource snapshotResource(@Nonnull Store store) { + return store.getResource(PhysicsSnapshotResource.getResourceType()); + } + + @Nullable + private static SpaceId spaceId(@Nonnull Store store, + @Nullable PhysicsBodySnapshot snapshot) { + if (snapshot == null) { + return null; + } + return store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .getSpaceId(snapshot.spaceUuid()); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsBodyEntities.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsBodyEntities.java new file mode 100644 index 00000000..f74cbdde --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsBodyEntities.java @@ -0,0 +1,154 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +/** + * Factories for PhysicsStore body entity holders. + */ +public final class PhysicsBodyEntities { + + private PhysicsBodyEntities() { + } + + @Nonnull + public static Holder dynamicBodyHolder(@Nonnull Ref spaceRef, + @Nonnull UUID bodyUuid, + @Nonnull Vector3f bodyCenter, + @Nonnull PhysicsShapeSpec shape, + float mass, + @Nonnull RigidBodySpawnSettings settings, + @Nullable Vector3f linearVelocity) { + return bodyHolder(spaceRef, + bodyUuid, + bodyCenter, + shape, + PhysicsBodyType.DYNAMIC, + mass, + settings, + linearVelocity); + } + + @Nonnull + public static Holder bodyHolder(@Nonnull Ref spaceRef, + @Nonnull UUID bodyUuid, + @Nonnull Vector3f bodyCenter, + @Nonnull PhysicsShapeSpec shape, + @Nonnull PhysicsBodyType bodyType, + float mass, + @Nonnull RigidBodySpawnSettings settings, + @Nullable Vector3f linearVelocity) { + Store store = requireSpaceStore(spaceRef); + return bodyHolderWithSpaceUuid(store, + PhysicsEntityRefs.entityUuid(spaceRef), + bodyUuid, + bodyCenter, + shape, + bodyType, + mass, + settings, + linearVelocity, + spaceRef); + } + + @Nonnull + private static Holder bodyHolderWithSpaceUuid(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull UUID bodyUuid, + @Nonnull Vector3f bodyCenter, + @Nonnull PhysicsShapeSpec shape, + @Nonnull PhysicsBodyType bodyType, + float mass, + @Nonnull RigidBodySpawnSettings settings, + @Nullable Vector3f linearVelocity, + @Nonnull Ref spaceRef) { + Objects.requireNonNull(spaceUuid, "spaceUuid"); + Objects.requireNonNull(bodyUuid, "bodyUuid"); + Objects.requireNonNull(bodyCenter, "bodyCenter"); + Objects.requireNonNull(shape, "shape"); + Objects.requireNonNull(bodyType, "bodyType"); + Objects.requireNonNull(settings, "settings"); + + BodyComponent body = new BodyComponent(spaceUuid); + body.setSpaceRef(spaceRef); + return PhysicsEntities.bodyHolder(store, + bodyUuid, + body, + new DynamicsComponent(bodyType, + mass, + settings.hasLinearDamping() ? settings.linearDamping() : 0.0f, + settings.hasAngularDamping() ? settings.angularDamping() : 0.0f, + false), + initialTarget(bodyCenter, linearVelocity), + new ColliderComponent(new Vector3f(), + new Quaternionf(), + settings.hasSensor() && settings.sensor()), + new ShapeComponent(shape.type(), + shape.halfExtentX(), + shape.halfExtentY(), + shape.halfExtentZ(), + shape.radius(), + shape.halfHeight(), + shape.axis(), + shape.groundY(), + ""), + new MaterialComponent(settings.hasFriction() ? settings.friction() : 0.5f, + settings.hasRestitution() ? settings.restitution() : 0.0f), + collisionFilter(settings)); + } + + @Nonnull + private static Store requireSpaceStore(@Nonnull Ref spaceRef) { + Ref checkedRef = Objects.requireNonNull(spaceRef, "spaceRef"); + Store store = checkedRef.getStore(); + PhysicsThreading.requireWorldThread(store, "build a PhysicsStore body entity holder"); + if (!checkedRef.isValid()) { + throw new IllegalStateException("PhysicsStore space ref is not valid: " + checkedRef); + } + return store; + } + + @Nonnull + private static TargetComponent initialTarget(@Nonnull Vector3f bodyCenter, + @Nullable Vector3f linearVelocity) { + TargetComponent target = new TargetComponent(); + target.setActive(false); + target.setPosition(bodyCenter); + target.setRotation(new Quaternionf()); + target.setLinearVelocity(linearVelocity != null ? linearVelocity : new Vector3f()); + target.setAngularVelocity(new Vector3f()); + target.setTransformEnabled(true); + target.setVelocityEnabled(linearVelocity != null); + target.setActivate(true); + return target; + } + + @Nonnull + private static CollisionFilterComponent collisionFilter(@Nonnull RigidBodySpawnSettings settings) { + return new CollisionFilterComponent( + settings.hasCollisionFilter() + ? settings.collisionGroup() + : PhysicsCollisionFilters.DYNAMIC_BODY, + settings.hasCollisionFilter() + ? settings.collisionMask() + : PhysicsCollisionFilters.ALL); + } + +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsCleanupHooks.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsCleanupHooks.java new file mode 100644 index 00000000..7b7135a7 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsCleanupHooks.java @@ -0,0 +1,179 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsStoreCleanupHooks; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3d; + +/** + * Cleanup extension points for optional Impulse plugins that own runtime state. + */ +public final class PhysicsCleanupHooks { + + @Nonnull + private static final Set ENTITY_STORE_CLEANUPS = + new CopyOnWriteArraySet<>(); + @Nonnull + private static final Set SELECTED_ENTITY_STORE_CLEANUPS = + new CopyOnWriteArraySet<>(); + @Nonnull + private static final Set>> + DETACHABLE_ENTITY_MARKERS = new CopyOnWriteArraySet<>(); + @Nonnull + private static final ConcurrentMap + BODY_ROW_CLEANUP_ADAPTERS = new ConcurrentHashMap<>(); + + private PhysicsCleanupHooks() { + } + + public static void registerEntityStoreCleanup(@Nonnull EntityStoreCleanup cleanup) { + ENTITY_STORE_CLEANUPS.add(Objects.requireNonNull(cleanup, "cleanup")); + } + + public static void unregisterEntityStoreCleanup(@Nonnull EntityStoreCleanup cleanup) { + ENTITY_STORE_CLEANUPS.remove(Objects.requireNonNull(cleanup, "cleanup")); + } + + public static int cleanupEntityStore(@Nonnull Store store) { + Objects.requireNonNull(store, "store"); + RuntimeException failure = null; + int removed = 0; + for (EntityStoreCleanup cleanup : ENTITY_STORE_CLEANUPS) { + try { + removed += cleanup.cleanup(store); + } catch (RuntimeException exception) { + failure = append(failure, exception); + } + } + throwIfFailed(failure); + return removed; + } + + public static void registerSelectedEntityStoreCleanup( + @Nonnull SelectedEntityStoreCleanup cleanup) { + SELECTED_ENTITY_STORE_CLEANUPS.add(Objects.requireNonNull(cleanup, "cleanup")); + } + + public static void unregisterSelectedEntityStoreCleanup( + @Nonnull SelectedEntityStoreCleanup cleanup) { + SELECTED_ENTITY_STORE_CLEANUPS.remove(Objects.requireNonNull(cleanup, "cleanup")); + } + + public static int cleanupSelectedEntityStore(@Nonnull Store store, + @Nonnull Set selectedBodyUuids, + @Nonnull Vector3d center, + double radiusSquared) { + Objects.requireNonNull(store, "store"); + Objects.requireNonNull(selectedBodyUuids, "selectedBodyUuids"); + Objects.requireNonNull(center, "center"); + RuntimeException failure = null; + int removed = 0; + for (SelectedEntityStoreCleanup cleanup : SELECTED_ENTITY_STORE_CLEANUPS) { + try { + removed += cleanup.cleanup(store, selectedBodyUuids, center, radiusSquared); + } catch (RuntimeException exception) { + failure = append(failure, exception); + } + } + throwIfFailed(failure); + return removed; + } + + public static void registerDetachableEntityMarker( + @Nonnull ComponentType> markerType) { + DETACHABLE_ENTITY_MARKERS.add(Objects.requireNonNull(markerType, "markerType")); + } + + public static void unregisterDetachableEntityMarker( + @Nonnull ComponentType> markerType) { + DETACHABLE_ENTITY_MARKERS.remove(Objects.requireNonNull(markerType, "markerType")); + } + + @Nonnull + public static List>> + detachableEntityMarkers() { + return List.copyOf(DETACHABLE_ENTITY_MARKERS); + } + + public static void registerBodyRuntimeCleanup( + @Nonnull Consumer> cleanup) { + PhysicsStoreCleanupHooks.registerBodyRuntimeCleanup(cleanup); + } + + public static void unregisterBodyRuntimeCleanup( + @Nonnull Consumer> cleanup) { + PhysicsStoreCleanupHooks.unregisterBodyRuntimeCleanup(cleanup); + } + + public static void registerBodyRowCleanup(@Nonnull BodyRowCleanup cleanup) { + BodyRowCleanup checkedCleanup = Objects.requireNonNull(cleanup, "cleanup"); + PhysicsStoreCleanupHooks.BodyRowCleanup adapter = + (store, bodyUuid, bodyRef) -> checkedCleanup.cleanup(store, bodyUuid, bodyRef); + PhysicsStoreCleanupHooks.BodyRowCleanup previous = + BODY_ROW_CLEANUP_ADAPTERS.putIfAbsent(checkedCleanup, adapter); + if (previous == null) { + PhysicsStoreCleanupHooks.registerBodyRowCleanup(adapter); + } + } + + public static void unregisterBodyRowCleanup(@Nonnull BodyRowCleanup cleanup) { + PhysicsStoreCleanupHooks.BodyRowCleanup adapter = + BODY_ROW_CLEANUP_ADAPTERS.remove(Objects.requireNonNull(cleanup, "cleanup")); + if (adapter != null) { + PhysicsStoreCleanupHooks.unregisterBodyRowCleanup(adapter); + } + } + + @Nullable + private static RuntimeException append(@Nullable RuntimeException failure, + @Nonnull RuntimeException exception) { + if (failure == null) { + return exception; + } + failure.addSuppressed(exception); + return failure; + } + + private static void throwIfFailed(@Nullable RuntimeException failure) { + if (failure != null) { + throw failure; + } + } + + @FunctionalInterface + public interface EntityStoreCleanup { + + int cleanup(@Nonnull Store store); + } + + @FunctionalInterface + public interface SelectedEntityStoreCleanup { + + int cleanup(@Nonnull Store store, + @Nonnull Set selectedBodyUuids, + @Nonnull Vector3d center, + double radiusSquared); + } + + @FunctionalInterface + public interface BodyRowCleanup { + + void cleanup(@Nonnull Store store, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsDiagnostics.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsDiagnostics.java new file mode 100644 index 00000000..91234586 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsDiagnostics.java @@ -0,0 +1,264 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletionStage; +import javax.annotation.Nonnull; + +/** + * Diagnostics for live PhysicsStore backend state. + * + *

The synchronous methods read mutable runtime/backend state and must only run from the + * owning PhysicsStore world thread. Off-thread callers should use the {@code *Async} methods, + * which enqueue copied reads on that world thread.

+ */ +public final class PhysicsDiagnostics { + + private PhysicsDiagnostics() { + } + + public static int bodyCount(@Nonnull Store store, + @Nonnull Ref spaceRef) { + PhysicsBackendAccess.SpaceContext space = + PhysicsBackendAccess.space(store, Objects.requireNonNull(spaceRef, "spaceRef")); + return space != null ? space.backendRuntime().bodyCount(space.spaceHandle().value()) : 0; + } + + @Nonnull + public static CompletionStage bodyCountAsync(@Nonnull World world, + @Nonnull Ref spaceRef) { + Objects.requireNonNull(spaceRef, "spaceRef"); + return PhysicsThreading.enqueueReadOnWorldThread(world, + "queue PhysicsStore body count read", + physics -> bodyCount(physics, spaceRef)); + } + + @Nonnull + public static CompletionStage bodyCountAsync(@Nonnull Store store, + @Nonnull Ref spaceRef) { + Objects.requireNonNull(spaceRef, "spaceRef"); + return PhysicsThreading.enqueueReadOnWorldThread(store, + "queue PhysicsStore body count read", + physics -> bodyCount(physics, spaceRef)); + } + + public static int runtimeJointCount(@Nonnull Store store) { + PhysicsThreading.requireBackendIdle(store, "read live PhysicsStore backend state"); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + JointCountCapture count = new JointCountCapture(); + runtime.forEachRuntimeSpaceBinding((_, _, spaceHandle, backendRuntime) -> + count.add(backendRuntime.jointCount(spaceHandle.value()))); + return count.value(); + } + + @Nonnull + public static CompletionStage runtimeJointCountAsync(@Nonnull World world) { + return PhysicsThreading.enqueueReadOnWorldThread(world, + "queue PhysicsStore joint count read", + PhysicsDiagnostics::runtimeJointCount); + } + + @Nonnull + public static CompletionStage runtimeJointCountAsync(@Nonnull Store store) { + return PhysicsThreading.enqueueReadOnWorldThread(store, + "queue PhysicsStore joint count read", + PhysicsDiagnostics::runtimeJointCount); + } + + public static boolean ccdSupported(@Nonnull Store store) { + PhysicsThreading.requireBackendIdle(store, "read live PhysicsStore backend state"); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + CcdSupportCapture supported = new CcdSupportCapture(); + runtime.forEachRuntimeSpaceBinding((_, _, spaceHandle, backendRuntime) -> { + if (backendRuntime.supportsContinuousCollision(spaceHandle.value())) { + supported.markSupported(); + } + }); + return supported.value(); + } + + @Nonnull + public static CompletionStage ccdSupportedAsync(@Nonnull World world) { + return PhysicsThreading.enqueueReadOnWorldThread(world, + "queue PhysicsStore CCD support read", + PhysicsDiagnostics::ccdSupported); + } + + @Nonnull + public static CompletionStage ccdSupportedAsync(@Nonnull Store store) { + return PhysicsThreading.enqueueReadOnWorldThread(store, + "queue PhysicsStore CCD support read", + PhysicsDiagnostics::ccdSupported); + } + + @Nonnull + public static SolverCapabilitySummary solverCapability(@Nonnull Store store, + @Nonnull Ref spaceRef) { + PhysicsThreading.requireBackendIdle(store, "read live PhysicsStore backend state"); + PhysicsSpaceCompatibilityIndexResource compatibility = store.getResource( + PhysicsSpaceCompatibilityIndexResource.getResourceType()); + PhysicsBackendAccess.SpaceContext space = + PhysicsBackendAccess.requireSpace(store, Objects.requireNonNull(spaceRef, "spaceRef")); + SpaceId spaceId = compatibility.getSpaceId(space.spaceUuid()); + if (spaceId == null) { + throw new IllegalArgumentException("Physics space ref=" + spaceRef + + " has no compatibility SpaceId"); + } + return solverCapability(spaceId, space); + } + + @Nonnull + public static CompletionStage solverCapabilityAsync( + @Nonnull World world, + @Nonnull Ref spaceRef) { + Objects.requireNonNull(spaceRef, "spaceRef"); + return PhysicsThreading.enqueueReadOnWorldThread(world, + "queue PhysicsStore solver capability read", + physics -> solverCapability(physics, spaceRef)); + } + + @Nonnull + public static CompletionStage solverCapabilityAsync( + @Nonnull Store store, + @Nonnull Ref spaceRef) { + Objects.requireNonNull(spaceRef, "spaceRef"); + return PhysicsThreading.enqueueReadOnWorldThread(store, + "queue PhysicsStore solver capability read", + physics -> solverCapability(physics, spaceRef)); + } + + @Nonnull + public static List spaceSummaries(@Nonnull Store store) { + PhysicsThreading.requireBackendIdle(store, "read live PhysicsStore backend state"); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + PhysicsSpaceCompatibilityIndexResource compatibility = store.getResource( + PhysicsSpaceCompatibilityIndexResource.getResourceType()); + List summaries = new ArrayList<>(); + runtime.forEachRuntimeSpaceBinding((spaceRef, _, _, _) -> { + PhysicsBackendAccess.SpaceContext context = + PhysicsBackendAccess.space(runtime, spaceRef); + if (context != null && compatibility.getSpaceId(context.spaceUuid()) != null) { + summaries.add(PhysicsBackendAccess.summary(compatibility, context)); + } + }); + return summaries.isEmpty() ? List.of() : List.copyOf(summaries); + } + + @Nonnull + public static CompletionStage> spaceSummariesAsync(@Nonnull World world) { + return PhysicsThreading.enqueueReadOnWorldThread(world, + "queue PhysicsStore space summaries read", + PhysicsDiagnostics::spaceSummaries); + } + + @Nonnull + public static List spaceSummaries(@Nonnull Store store, + @Nonnull Ref spaceRef) { + PhysicsThreading.requireBackendIdle(store, "read live PhysicsStore backend state"); + PhysicsSpaceCompatibilityIndexResource compatibility = store.getResource( + PhysicsSpaceCompatibilityIndexResource.getResourceType()); + PhysicsBackendAccess.SpaceContext space = + PhysicsBackendAccess.space(store, Objects.requireNonNull(spaceRef, "spaceRef")); + return space != null && compatibility.getSpaceId(space.spaceUuid()) != null + ? List.of(PhysicsBackendAccess.summary(compatibility, space)) + : List.of(); + } + + @Nonnull + public static CompletionStage> spaceSummariesAsync(@Nonnull World world, + @Nonnull Ref spaceRef) { + Objects.requireNonNull(spaceRef, "spaceRef"); + return PhysicsThreading.enqueueReadOnWorldThread(world, + "queue PhysicsStore space summary read", + physics -> spaceSummaries(physics, spaceRef)); + } + + @Nonnull + public static CompletionStage> spaceSummariesAsync( + @Nonnull Store store, + @Nonnull Ref spaceRef) { + Objects.requireNonNull(spaceRef, "spaceRef"); + return PhysicsThreading.enqueueReadOnWorldThread(store, + "queue PhysicsStore space summary read", + physics -> spaceSummaries(physics, spaceRef)); + } + + @Nonnull + public static List unsupportedCcdSpaces(@Nonnull Store store) { + PhysicsThreading.requireBackendIdle(store, "read live PhysicsStore backend state"); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + PhysicsSpaceCompatibilityIndexResource compatibility = store.getResource( + PhysicsSpaceCompatibilityIndexResource.getResourceType()); + List spaces = new ArrayList<>(); + runtime.forEachRuntimeSpaceBinding((spaceRef, _, spaceHandle, backendRuntime) -> { + if (!backendRuntime.supportsContinuousCollision(spaceHandle.value())) { + PhysicsBackendAccess.SpaceContext context = + PhysicsBackendAccess.space(runtime, spaceRef); + if (context != null) { + spaces.add(PhysicsBackendAccess.summary(compatibility, context)); + } + } + }); + return spaces.isEmpty() ? List.of() : List.copyOf(spaces); + } + + @Nonnull + public static CompletionStage> unsupportedCcdSpacesAsync( + @Nonnull World world) { + return PhysicsThreading.enqueueReadOnWorldThread(world, + "queue PhysicsStore unsupported CCD spaces read", + PhysicsDiagnostics::unsupportedCcdSpaces); + } + + @Nonnull + public static CompletionStage> unsupportedCcdSpacesAsync( + @Nonnull Store store) { + return PhysicsThreading.enqueueReadOnWorldThread(store, + "queue PhysicsStore unsupported CCD spaces read", + PhysicsDiagnostics::unsupportedCcdSpaces); + } + + @Nonnull + private static SolverCapabilitySummary solverCapability(@Nonnull SpaceId spaceId, + @Nonnull PhysicsBackendAccess.SpaceContext space) { + return new SolverCapabilitySummary(spaceId, + space.backendId().value(), + space.backendRuntime().supportsSolverTuning(space.spaceHandle().value()), + space.backendRuntime().supportsActivationTuning(space.spaceHandle().value())); + } + + private static final class JointCountCapture { + + private int value; + + private void add(int count) { + value += count; + } + + private int value() { + return value; + } + } + + private static final class CcdSupportCapture { + + private boolean value; + + private void markSupported() { + value = true; + } + + private boolean value() { + return value; + } + } + +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsEntities.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsEntities.java new file mode 100644 index 00000000..3f8ef3d5 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsEntities.java @@ -0,0 +1,385 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.CollisionLodSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.ExtensionSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.SolverSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.VisualMaterializationSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.VisualSyncSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.ChunkCollisionSettingsComponent; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Holder/component helpers for authoritative PhysicsStore aggregate entities. + */ +public final class PhysicsEntities { + + private PhysicsEntities() { + } + + @Nonnull + public static Holder entityHolder(@Nonnull Store store, + @Nonnull UUID entityUuid) { + Holder holder = store.getRegistry().newHolder(); + addUuid(holder, entityUuid); + return holder; + } + + public static void addUuid(@Nonnull Holder holder, + @Nonnull UUID entityUuid) { + Objects.requireNonNull(holder, "holder") + .addComponent(UuidComponent.getComponentType(), + new UuidComponent(Objects.requireNonNull(entityUuid, "entityUuid"))); + } + + @Nullable + public static Ref resolveRef(@Nonnull Store store, + @Nonnull UUID entityUuid) { + Store checkedStore = Objects.requireNonNull(store, "store"); + PhysicsThreading.requireWorldThread(checkedStore, "resolve a PhysicsStore entity ref"); + Ref ref = checkedStore.getExternalData() + .getRefFromUUID(Objects.requireNonNull(entityUuid, "entityUuid")); + return ref != null && ref.getStore() == checkedStore && ref.isValid() ? ref : null; + } + + @Nonnull + public static Holder spaceHolder(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull SpaceComponent space) { + Holder holder = entityHolder(store, spaceUuid); + addSpaceComponent(holder, space); + return holder; + } + + @Nonnull + public static Holder spaceHolder(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull SpaceComponent space, + @Nonnull ChunkCollisionSettingsComponent chunkCollisionSettings, + @Nonnull SolverSettingsComponent solverSettings, + @Nonnull VisualSyncSettingsComponent visualSyncSettings, + @Nonnull VisualMaterializationSettingsComponent visualMaterializationSettings, + @Nonnull CollisionLodSettingsComponent collisionLodSettings, + @Nonnull ExtensionSettingsComponent extensionSettings) { + Holder holder = entityHolder(store, spaceUuid); + addSpaceComponents(holder, + space, + chunkCollisionSettings, + solverSettings, + visualSyncSettings, + visualMaterializationSettings, + collisionLodSettings, + extensionSettings); + return holder; + } + + public static void addSpaceComponent(@Nonnull Holder holder, + @Nonnull SpaceComponent space) { + Objects.requireNonNull(holder, "holder") + .addComponent(SpaceComponent.getComponentType(), + Objects.requireNonNull(space, "space").clone()); + } + + @Nonnull + public static Holder bodyHolder(@Nonnull Store store, + @Nonnull UUID bodyUuid, + @Nonnull BodyComponent body, + @Nonnull DynamicsComponent dynamics, + @Nullable TargetComponent target, + @Nonnull ColliderComponent collider, + @Nonnull ShapeComponent shape, + @Nonnull MaterialComponent material, + @Nonnull CollisionFilterComponent filter) { + Holder holder = entityHolder(store, bodyUuid); + addBodyComponents(holder, body, dynamics, target, collider, shape, material, filter); + return holder; + } + + @Nonnull + public static Holder jointHolder(@Nonnull Store store, + @Nonnull UUID jointUuid, + @Nonnull JointComponent joint) { + Holder holder = entityHolder(store, jointUuid); + holder.addComponent(JointComponent.getComponentType(), + Objects.requireNonNull(joint, "joint").clone()); + return holder; + } + + public static void addSpaceComponents(@Nonnull Holder holder, + @Nonnull SpaceComponent space, + @Nonnull ChunkCollisionSettingsComponent chunkCollisionSettings, + @Nonnull SolverSettingsComponent solverSettings, + @Nonnull VisualSyncSettingsComponent visualSyncSettings, + @Nonnull VisualMaterializationSettingsComponent visualMaterializationSettings, + @Nonnull CollisionLodSettingsComponent collisionLodSettings, + @Nonnull ExtensionSettingsComponent extensionSettings) { + addSpaceComponent(holder, space); + addChunkCollisionSettingsComponent(holder, chunkCollisionSettings); + addSpaceSettingsComponents(holder, + solverSettings, + visualSyncSettings, + visualMaterializationSettings, + collisionLodSettings, + extensionSettings); + } + + public static void addChunkCollisionSettingsComponent(@Nonnull Holder holder, + @Nonnull ChunkCollisionSettingsComponent chunkCollisionSettings) { + ChunkCollisionSettingsComponent component = + Objects.requireNonNull(chunkCollisionSettings, "chunkCollisionSettings"); + addIfNonDefault(holder, + ChunkCollisionSettingsComponent.getComponentType(), + component, + component.isDefault()); + } + + public static void addSpaceSettingsComponents(@Nonnull Holder holder, + @Nonnull SolverSettingsComponent solverSettings, + @Nonnull VisualSyncSettingsComponent visualSyncSettings, + @Nonnull VisualMaterializationSettingsComponent visualMaterializationSettings, + @Nonnull CollisionLodSettingsComponent collisionLodSettings, + @Nonnull ExtensionSettingsComponent extensionSettings) { + SolverSettingsComponent solver = Objects.requireNonNull(solverSettings, + "solverSettings"); + addIfNonDefault(holder, + SolverSettingsComponent.getComponentType(), + solver, + solver.isDefault()); + VisualSyncSettingsComponent visualSync = Objects.requireNonNull(visualSyncSettings, + "visualSyncSettings"); + addIfNonDefault(holder, + VisualSyncSettingsComponent.getComponentType(), + visualSync, + visualSync.isDefault()); + VisualMaterializationSettingsComponent visualMaterialization = Objects.requireNonNull( + visualMaterializationSettings, + "visualMaterializationSettings"); + addIfNonDefault(holder, + VisualMaterializationSettingsComponent.getComponentType(), + visualMaterialization, + visualMaterialization.isDefault()); + CollisionLodSettingsComponent collisionLod = Objects.requireNonNull(collisionLodSettings, + "collisionLodSettings"); + addIfNonDefault(holder, + CollisionLodSettingsComponent.getComponentType(), + collisionLod, + collisionLod.isDefault()); + ExtensionSettingsComponent extension = Objects.requireNonNull(extensionSettings, + "extensionSettings"); + addIfNonDefault(holder, + ExtensionSettingsComponent.getComponentType(), + extension, + extension.isDefault()); + } + + public static void addBodyComponents(@Nonnull Holder holder, + @Nonnull BodyComponent body, + @Nonnull DynamicsComponent dynamics, + @Nullable TargetComponent target, + @Nonnull ColliderComponent collider, + @Nonnull ShapeComponent shape, + @Nonnull MaterialComponent material, + @Nonnull CollisionFilterComponent filter) { + Objects.requireNonNull(holder, "holder") + .addComponent(BodyComponent.getComponentType(), + Objects.requireNonNull(body, "body").clone()); + holder.addComponent(DynamicsComponent.getComponentType(), + Objects.requireNonNull(dynamics, "dynamics").clone()); + holder.addComponent(ColliderComponent.getComponentType(), + Objects.requireNonNull(collider, "collider").clone()); + holder.addComponent(ShapeComponent.getComponentType(), + Objects.requireNonNull(shape, "shape").clone()); + holder.addComponent(MaterialComponent.getComponentType(), + Objects.requireNonNull(material, "material").clone()); + holder.addComponent(CollisionFilterComponent.getComponentType(), + Objects.requireNonNull(filter, "filter").clone()); + if (target != null) { + holder.addComponent(TargetComponent.getComponentType(), target.clone()); + } + } + + public static void putSpaceComponents(@Nonnull Store store, + @Nonnull Ref ref, + @Nonnull SpaceComponent space, + @Nonnull ChunkCollisionSettingsComponent chunkCollisionSettings, + @Nonnull SolverSettingsComponent solverSettings, + @Nonnull VisualSyncSettingsComponent visualSyncSettings, + @Nonnull VisualMaterializationSettingsComponent visualMaterializationSettings, + @Nonnull CollisionLodSettingsComponent collisionLodSettings, + @Nonnull ExtensionSettingsComponent extensionSettings) { + Store checkedStore = Objects.requireNonNull(store, "store"); + PhysicsThreading.requireWorldThread(checkedStore, "put PhysicsStore space components"); + Objects.requireNonNull(ref, "ref"); + checkedStore.putComponent(ref, + SpaceComponent.getComponentType(), + Objects.requireNonNull(space, "space").clone()); + putChunkCollisionSettingsComponent(checkedStore, ref, chunkCollisionSettings); + putSpaceSettingsComponents(checkedStore, + ref, + solverSettings, + visualSyncSettings, + visualMaterializationSettings, + collisionLodSettings, + extensionSettings); + } + + public static void putChunkCollisionSettingsComponent(@Nonnull Store store, + @Nonnull Ref ref, + @Nonnull ChunkCollisionSettingsComponent chunkCollisionSettings) { + Store checkedStore = Objects.requireNonNull(store, "store"); + PhysicsThreading.requireWorldThread(checkedStore, + "put PhysicsStore chunk collision settings component"); + ChunkCollisionSettingsComponent component = + Objects.requireNonNull(chunkCollisionSettings, "chunkCollisionSettings"); + putOrRemoveDefault(checkedStore, + Objects.requireNonNull(ref, "ref"), + ChunkCollisionSettingsComponent.getComponentType(), + component, + component.isDefault()); + } + + public static void putSpaceSettingsComponents(@Nonnull Store store, + @Nonnull Ref ref, + @Nonnull SolverSettingsComponent solverSettings, + @Nonnull VisualSyncSettingsComponent visualSyncSettings, + @Nonnull VisualMaterializationSettingsComponent visualMaterializationSettings, + @Nonnull CollisionLodSettingsComponent collisionLodSettings, + @Nonnull ExtensionSettingsComponent extensionSettings) { + Store checkedStore = Objects.requireNonNull(store, "store"); + PhysicsThreading.requireWorldThread(checkedStore, + "put PhysicsStore space settings components"); + Objects.requireNonNull(ref, "ref"); + SolverSettingsComponent solver = Objects.requireNonNull(solverSettings, + "solverSettings"); + putOrRemoveDefault(checkedStore, + ref, + SolverSettingsComponent.getComponentType(), + solver, + solver.isDefault()); + VisualSyncSettingsComponent visualSync = Objects.requireNonNull(visualSyncSettings, + "visualSyncSettings"); + putOrRemoveDefault(checkedStore, + ref, + VisualSyncSettingsComponent.getComponentType(), + visualSync, + visualSync.isDefault()); + VisualMaterializationSettingsComponent visualMaterialization = Objects.requireNonNull( + visualMaterializationSettings, + "visualMaterializationSettings"); + putOrRemoveDefault(checkedStore, + ref, + VisualMaterializationSettingsComponent.getComponentType(), + visualMaterialization, + visualMaterialization.isDefault()); + CollisionLodSettingsComponent collisionLod = Objects.requireNonNull(collisionLodSettings, + "collisionLodSettings"); + putOrRemoveDefault(checkedStore, + ref, + CollisionLodSettingsComponent.getComponentType(), + collisionLod, + collisionLod.isDefault()); + ExtensionSettingsComponent extension = Objects.requireNonNull(extensionSettings, + "extensionSettings"); + putOrRemoveDefault(checkedStore, + ref, + ExtensionSettingsComponent.getComponentType(), + extension, + extension.isDefault()); + } + + public static void putBodyComponents(@Nonnull Store store, + @Nonnull Ref ref, + @Nonnull BodyComponent body, + @Nonnull DynamicsComponent dynamics, + @Nullable TargetComponent target, + @Nonnull ColliderComponent collider, + @Nonnull ShapeComponent shape, + @Nonnull MaterialComponent material, + @Nonnull CollisionFilterComponent filter) { + Store checkedStore = Objects.requireNonNull(store, "store"); + PhysicsThreading.requireWorldThread(checkedStore, "put PhysicsStore body components"); + Objects.requireNonNull(ref, "ref"); + checkedStore.putComponent(ref, + BodyComponent.getComponentType(), + Objects.requireNonNull(body, "body").clone()); + checkedStore.putComponent(ref, + DynamicsComponent.getComponentType(), + Objects.requireNonNull(dynamics, "dynamics").clone()); + checkedStore.putComponent(ref, + ColliderComponent.getComponentType(), + Objects.requireNonNull(collider, "collider").clone()); + checkedStore.putComponent(ref, + ShapeComponent.getComponentType(), + Objects.requireNonNull(shape, "shape").clone()); + checkedStore.putComponent(ref, + MaterialComponent.getComponentType(), + Objects.requireNonNull(material, "material").clone()); + checkedStore.putComponent(ref, + CollisionFilterComponent.getComponentType(), + Objects.requireNonNull(filter, "filter").clone()); + if (target != null) { + checkedStore.putComponent(ref, TargetComponent.getComponentType(), target.clone()); + } else { + checkedStore.removeComponent(ref, TargetComponent.getComponentType()); + } + } + + public static void putJointComponent(@Nonnull Store store, + @Nonnull Ref ref, + @Nonnull JointComponent joint) { + Store checkedStore = Objects.requireNonNull(store, "store"); + PhysicsThreading.requireWorldThread(checkedStore, "put PhysicsStore joint component"); + checkedStore + .putComponent(Objects.requireNonNull(ref, "ref"), + JointComponent.getComponentType(), + Objects.requireNonNull(joint, "joint").clone()); + } + + private static > void addIfNonDefault( + @Nonnull Holder holder, + @Nonnull ComponentType componentType, + @Nonnull T component, + boolean defaultValue) { + if (!defaultValue) { + Objects.requireNonNull(holder, "holder").addComponent(componentType, copy(component)); + } + } + + private static > void putOrRemoveDefault( + @Nonnull Store store, + @Nonnull Ref ref, + @Nonnull ComponentType componentType, + @Nonnull T component, + boolean defaultValue) { + if (defaultValue) { + store.removeComponentIfExists(ref, componentType); + } else { + store.putComponent(ref, componentType, copy(component)); + } + } + + @Nonnull + @SuppressWarnings("unchecked") + private static > T copy(@Nonnull T component) { + return (T) component.clone(); + } + +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsEntityRefs.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsEntityRefs.java new file mode 100644 index 00000000..e221c914 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsEntityRefs.java @@ -0,0 +1,40 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; + +final class PhysicsEntityRefs { + + private PhysicsEntityRefs() { + } + + @Nonnull + static UUID entityUuid(@Nonnull Ref ref) { + Ref checkedRef = Objects.requireNonNull(ref, "ref"); + Store store = checkedRef.getStore(); + PhysicsThreading.requireWorldThread(store, "read a PhysicsStore entity UUID"); + if (!checkedRef.isValid()) { + throw new IllegalStateException("PhysicsStore entity ref is not valid: " + checkedRef); + } + UuidComponent uuid = store.getComponent(checkedRef, UuidComponent.getComponentType()); + if (uuid == null) { + throw new IllegalStateException("PhysicsStore entity has no UUID component: " + checkedRef); + } + return uuid.getUuid(); + } + + static void requireSameStore(@Nonnull Ref expectedStoreRef, + @Nonnull Ref ref, + @Nonnull String name) { + if (Objects.requireNonNull(ref, name).getStore() + != Objects.requireNonNull(expectedStoreRef, "expectedStoreRef").getStore()) { + throw new IllegalArgumentException("PhysicsStore entity ref belongs to a different store: " + + name); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsJointEntities.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsJointEntities.java new file mode 100644 index 00000000..f956ad83 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsJointEntities.java @@ -0,0 +1,62 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointType; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Vector3f; + +/** + * Factories for direct PhysicsStore joint components. + */ +public final class PhysicsJointEntities { + + private PhysicsJointEntities() { + } + + @Nonnull + public static JointComponent joint(@Nonnull Ref spaceRef, + @Nonnull Ref bodyARef, + @Nonnull Ref bodyBRef, + @Nonnull JointType type, + @Nonnull Vector3f anchorA, + @Nonnull Vector3f anchorB, + @Nonnull Vector3f axis) { + PhysicsEntityRefs.requireSameStore(spaceRef, bodyARef, "bodyARef"); + PhysicsEntityRefs.requireSameStore(spaceRef, bodyBRef, "bodyBRef"); + JointComponent joint = jointWithUuids(PhysicsEntityRefs.entityUuid(spaceRef), + PhysicsEntityRefs.entityUuid(bodyARef), + PhysicsEntityRefs.entityUuid(bodyBRef), + type, + anchorA, + anchorB, + axis); + joint.setSpaceRef(spaceRef); + joint.setBodyARef(bodyARef); + joint.setBodyBRef(bodyBRef); + return joint; + } + + @Nonnull + private static JointComponent jointWithUuids(@Nonnull UUID spaceUuid, + @Nonnull UUID bodyAUuid, + @Nonnull UUID bodyBUuid, + @Nonnull JointType type, + @Nonnull Vector3f anchorA, + @Nonnull Vector3f anchorB, + @Nonnull Vector3f axis) { + JointComponent joint = new JointComponent(); + joint.setSpaceUuid(Objects.requireNonNull(spaceUuid, "spaceUuid")); + joint.setBodyAUuid(Objects.requireNonNull(bodyAUuid, "bodyAUuid")); + joint.setBodyBUuid(Objects.requireNonNull(bodyBUuid, "bodyBUuid")); + joint.setType(Objects.requireNonNull(type, "type")); + joint.setAnchorA(anchorA); + joint.setAnchorB(anchorB); + joint.setAxis(axis); + joint.setEnabled(true); + return joint; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsRaycasts.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsRaycasts.java new file mode 100644 index 00000000..2eeabde9 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsRaycasts.java @@ -0,0 +1,286 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.runtime.BackendRayHitSink; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.CompletionStage; +import javax.annotation.Nonnull; +import org.joml.Vector3f; + +/** + * PhysicsStore live backend raycasts. + * + *

The synchronous methods read live backend state through {@link PhysicsRuntimeResource} and + * must only be called from the owning PhysicsStore world thread. Off-thread callers should use the + * {@code *Async} methods, which copy inputs, enqueue the read on that world thread, and complete + * with copied hit views.

+ */ +public final class PhysicsRaycasts { + + private PhysicsRaycasts() { + } + + @Nonnull + public static Optional closest(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull Vector3f from, + @Nonnull Vector3f to) { + PhysicsBackendAccess.SpaceContext space = + PhysicsBackendAccess.space(store, Objects.requireNonNull(spaceRef, "spaceRef")); + return space != null ? closest(store, space, from, to) : Optional.empty(); + } + + @Nonnull + public static List all(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull Vector3f from, + @Nonnull Vector3f to) { + PhysicsBackendAccess.SpaceContext space = + PhysicsBackendAccess.space(store, Objects.requireNonNull(spaceRef, "spaceRef")); + return space != null ? all(store, space, from, to) : List.of(); + } + + @Nonnull + public static RaycastClosestBatchResult closestBatch(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull List rays) { + PhysicsBackendAccess.SpaceContext space = + PhysicsBackendAccess.space(store, Objects.requireNonNull(spaceRef, "spaceRef")); + return closestBatch(store, space, rays); + } + + @Nonnull + public static CompletionStage> closestAsync(@Nonnull World world, + @Nonnull Ref spaceRef, + @Nonnull Vector3f from, + @Nonnull Vector3f to) { + Vector3f copiedFrom = new Vector3f(Objects.requireNonNull(from, "from")); + Vector3f copiedTo = new Vector3f(Objects.requireNonNull(to, "to")); + Objects.requireNonNull(spaceRef, "spaceRef"); + return PhysicsThreading.enqueueReadOnWorldThread(world, + "queue PhysicsStore closest raycast read", + physics -> closest(physics, spaceRef, copiedFrom, copiedTo)); + } + + @Nonnull + public static CompletionStage> closestAsync( + @Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull Vector3f from, + @Nonnull Vector3f to) { + Vector3f copiedFrom = new Vector3f(Objects.requireNonNull(from, "from")); + Vector3f copiedTo = new Vector3f(Objects.requireNonNull(to, "to")); + Objects.requireNonNull(spaceRef, "spaceRef"); + return PhysicsThreading.enqueueReadOnWorldThread(store, + "queue PhysicsStore closest raycast read", + physics -> closest(physics, spaceRef, copiedFrom, copiedTo)); + } + + @Nonnull + public static CompletionStage> allAsync(@Nonnull World world, + @Nonnull Ref spaceRef, + @Nonnull Vector3f from, + @Nonnull Vector3f to) { + Vector3f copiedFrom = new Vector3f(Objects.requireNonNull(from, "from")); + Vector3f copiedTo = new Vector3f(Objects.requireNonNull(to, "to")); + Objects.requireNonNull(spaceRef, "spaceRef"); + return PhysicsThreading.enqueueReadOnWorldThread(world, + "queue PhysicsStore all raycast read", + physics -> all(physics, spaceRef, copiedFrom, copiedTo)); + } + + @Nonnull + public static CompletionStage> allAsync(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull Vector3f from, + @Nonnull Vector3f to) { + Vector3f copiedFrom = new Vector3f(Objects.requireNonNull(from, "from")); + Vector3f copiedTo = new Vector3f(Objects.requireNonNull(to, "to")); + Objects.requireNonNull(spaceRef, "spaceRef"); + return PhysicsThreading.enqueueReadOnWorldThread(store, + "queue PhysicsStore all raycast read", + physics -> all(physics, spaceRef, copiedFrom, copiedTo)); + } + + @Nonnull + public static CompletionStage closestBatchAsync( + @Nonnull World world, + @Nonnull Ref spaceRef, + @Nonnull List rays) { + List copied = List.copyOf(Objects.requireNonNull(rays, "rays")); + Objects.requireNonNull(spaceRef, "spaceRef"); + return PhysicsThreading.enqueueReadOnWorldThread(world, + "queue PhysicsStore batch raycast read", + physics -> closestBatch(physics, spaceRef, copied)); + } + + @Nonnull + public static CompletionStage closestBatchAsync( + @Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull List rays) { + List copied = List.copyOf(Objects.requireNonNull(rays, "rays")); + Objects.requireNonNull(spaceRef, "spaceRef"); + return PhysicsThreading.enqueueReadOnWorldThread(store, + "queue PhysicsStore batch raycast read", + physics -> closestBatch(physics, spaceRef, copied)); + } + + @Nonnull + private static Optional closest(@Nonnull Store store, + @Nonnull PhysicsBackendAccess.SpaceContext space, + @Nonnull Vector3f from, + @Nonnull Vector3f to) { + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + RayHitCapture hit = new RayHitCapture(runtime, space); + Vector3f copiedFrom = new Vector3f(Objects.requireNonNull(from, "from")); + Vector3f copiedTo = new Vector3f(Objects.requireNonNull(to, "to")); + boolean hitFound = space.backendRuntime().raycastClosest(space.spaceHandle().value(), + copiedFrom.x, + copiedFrom.y, + copiedFrom.z, + copiedTo.x, + copiedTo.y, + copiedTo.z, + hit); + return hitFound && hit.captured ? Optional.of(hit.view()) : Optional.empty(); + } + + @Nonnull + private static List all(@Nonnull Store store, + @Nonnull PhysicsBackendAccess.SpaceContext space, + @Nonnull Vector3f from, + @Nonnull Vector3f to) { + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + Vector3f copiedFrom = new Vector3f(Objects.requireNonNull(from, "from")); + Vector3f copiedTo = new Vector3f(Objects.requireNonNull(to, "to")); + List hits = new ArrayList<>(); + space.backendRuntime().raycastAll(space.spaceHandle().value(), + copiedFrom.x, + copiedFrom.y, + copiedFrom.z, + copiedTo.x, + copiedTo.y, + copiedTo.z, + (bodyId, + pointX, + pointY, + pointZ, + normalX, + normalY, + normalZ, + fraction, + distance) -> hits.add(PhysicsBackendAccess.toView(runtime, + space, + bodyId, + pointX, + pointY, + pointZ, + normalX, + normalY, + normalZ, + fraction, + distance))); + return hits.isEmpty() ? List.of() : List.copyOf(hits); + } + + @Nonnull + private static RaycastClosestBatchResult closestBatch(@Nonnull Store store, + PhysicsBackendAccess.SpaceContext space, + @Nonnull List rays) { + List copiedRays = List.copyOf(Objects.requireNonNull(rays, "rays")); + int rayCount = copiedRays.size(); + RaycastHitView[] hits = new RaycastHitView[rayCount]; + if (space == null) { + return new RaycastClosestBatchResult(hits); + } + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + Vector3f from = new Vector3f(); + Vector3f to = new Vector3f(); + for (int index = 0; index < rayCount; index++) { + RaycastSegment ray = copiedRays.get(index); + ray.copyFrom(from); + ray.copyTo(to); + int rayIndex = index; + space.backendRuntime().raycastClosest(space.spaceHandle().value(), + from.x, + from.y, + from.z, + to.x, + to.y, + to.z, + (bodyId, + pointX, + pointY, + pointZ, + normalX, + normalY, + normalZ, + fraction, + distance) -> hits[rayIndex] = PhysicsBackendAccess.toView(runtime, + space, + bodyId, + pointX, + pointY, + pointZ, + normalX, + normalY, + normalZ, + fraction, + distance)); + } + return new RaycastClosestBatchResult(hits); + } + + private static final class RayHitCapture implements BackendRayHitSink { + + @Nonnull + private final PhysicsRuntimeResource runtime; + @Nonnull + private final PhysicsBackendAccess.SpaceContext space; + private boolean captured; + private RaycastHitView view; + + private RayHitCapture(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull PhysicsBackendAccess.SpaceContext space) { + this.runtime = runtime; + this.space = space; + } + + @Override + public void accept(long bodyId, + float pointX, + float pointY, + float pointZ, + float normalX, + float normalY, + float normalZ, + float fraction, + float distance) { + view = PhysicsBackendAccess.toView(runtime, + space, + bodyId, + pointX, + pointY, + pointZ, + normalX, + normalY, + normalZ, + fraction, + distance); + captured = true; + } + + @Nonnull + private RaycastHitView view() { + return Objects.requireNonNull(view, "view"); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsShapeSpec.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsShapeSpec.java similarity index 99% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsShapeSpec.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsShapeSpec.java index 70e9c8f7..51bd62f9 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsShapeSpec.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsShapeSpec.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; +package dev.hytalemodding.impulse.core.plugin.physics; import dev.hytalemodding.impulse.api.PhysicsAxis; import dev.hytalemodding.impulse.api.ShapeType; diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsSpaces.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsSpaces.java new file mode 100644 index 00000000..d7291987 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsSpaces.java @@ -0,0 +1,522 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsSpaceMutations; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsTopologyMutations; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.plugin.components.ExtensionSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.components.SolverSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.VisualMaterializationSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.VisualSyncSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.ChunkCollisionSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.CollisionLodSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsChunkCollisionSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsCollisionLodSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualMaterializationSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualSyncSettings; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsExtensionSettings; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSolverSettings; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Public compatibility reads for PhysicsStore space entities. + */ +public final class PhysicsSpaces { + + private PhysicsSpaces() { + } + + @Nullable + public static Ref resolveRef(@Nonnull Store store, + @Nonnull SpaceId spaceId) { + Store checkedStore = requireWorldThread(store, "resolve a PhysicsStore space ref"); + UUID spaceUuid = checkedStore + .getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .getSpaceUuid(Objects.requireNonNull(spaceId, "spaceId")); + if (spaceUuid == null) { + return null; + } + Ref ref = checkedStore.getExternalData().getRefFromUUID(spaceUuid); + return ref != null && ref.getStore() == checkedStore && ref.isValid() ? ref : null; + } + + public static boolean hasSpace(@Nonnull Store store, + @Nonnull SpaceId spaceId) { + Store checkedStore = requireWorldThread(store, "check a PhysicsStore space"); + return checkedStore.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .hasSpace(Objects.requireNonNull(spaceId, "spaceId")); + } + + @Nonnull + public static SpaceId create(@Nonnull Store store, + @Nonnull BackendId backendId) { + SpaceId spaceId = SpaceId.next(); + create(store, UUID.randomUUID(), spaceId, backendId); + return spaceId; + } + + @Nonnull + public static Ref create(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull SpaceId spaceId, + @Nonnull BackendId backendId) { + Store checkedStore = requireWorldThread(store, + "create a PhysicsStore space"); + return PhysicsSpaceMutations.addSpace(checkedStore, + Objects.requireNonNull(spaceUuid, "spaceUuid"), + Objects.requireNonNull(spaceId, "spaceId"), + Objects.requireNonNull(backendId, "backendId")); + } + + @Nonnull + public static Collection spaceIds(@Nonnull Store store) { + Store checkedStore = requireWorldThread(store, "list PhysicsStore spaces"); + return List.copyOf(checkedStore + .getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .spaceIds()); + } + + public static int count(@Nonnull Store store) { + Store checkedStore = requireWorldThread(store, "count PhysicsStore spaces"); + return checkedStore.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .size(); + } + + @Nullable + public static PhysicsChunkCollisionSettings chunkCollisionSettings( + @Nonnull Store store, + @Nonnull SpaceId spaceId) { + Ref ref = resolveRef(store, spaceId); + return ref != null ? chunkCollisionSettings(store, ref) : null; + } + + @Nullable + public static PhysicsChunkCollisionSettings chunkCollisionSettings( + @Nonnull Store store, + @Nonnull Ref spaceRef) { + Store checkedStore = requireWorldThread(store, + "read PhysicsStore chunk collision settings"); + Ref checkedRef = Objects.requireNonNull(spaceRef, "spaceRef"); + if (!isSpaceRef(checkedStore, checkedRef)) { + return null; + } + PhysicsChunkCollisionSettings settings = new PhysicsChunkCollisionSettings(); + ChunkCollisionSettingsComponent component = checkedStore.getComponent(checkedRef, + ChunkCollisionSettingsComponent.getComponentType()); + if (component != null) { + component.copyTo(settings); + } + return settings; + } + + @Nullable + public static PhysicsSolverSettings solverSettings(@Nonnull Store store, + @Nonnull SpaceId spaceId) { + Ref ref = resolveRef(store, spaceId); + return ref != null ? solverSettings(store, ref) : null; + } + + @Nullable + public static PhysicsSolverSettings solverSettings(@Nonnull Store store, + @Nonnull Ref spaceRef) { + Store checkedStore = requireWorldThread(store, + "read PhysicsStore solver settings"); + Ref checkedRef = Objects.requireNonNull(spaceRef, "spaceRef"); + if (!isSpaceRef(checkedStore, checkedRef)) { + return null; + } + PhysicsSolverSettings settings = new PhysicsSolverSettings(); + SolverSettingsComponent component = checkedStore.getComponent(checkedRef, + SolverSettingsComponent.getComponentType()); + if (component != null) { + component.copyTo(settings); + } + return settings; + } + + @Nullable + public static PhysicsVisualSyncSettings visualSyncSettings( + @Nonnull Store store, + @Nonnull SpaceId spaceId) { + Ref ref = resolveRef(store, spaceId); + return ref != null ? visualSyncSettings(store, ref) : null; + } + + @Nullable + public static PhysicsVisualSyncSettings visualSyncSettings( + @Nonnull Store store, + @Nonnull Ref spaceRef) { + Store checkedStore = requireWorldThread(store, + "read PhysicsStore visual sync settings"); + Ref checkedRef = Objects.requireNonNull(spaceRef, "spaceRef"); + if (!isSpaceRef(checkedStore, checkedRef)) { + return null; + } + PhysicsVisualSyncSettings settings = new PhysicsVisualSyncSettings(); + VisualSyncSettingsComponent component = checkedStore.getComponent(checkedRef, + VisualSyncSettingsComponent.getComponentType()); + if (component != null) { + component.copyTo(settings); + } + return settings; + } + + @Nullable + public static PhysicsVisualMaterializationSettings visualMaterializationSettings( + @Nonnull Store store, + @Nonnull SpaceId spaceId) { + Ref ref = resolveRef(store, spaceId); + return ref != null ? visualMaterializationSettings(store, ref) : null; + } + + @Nullable + public static PhysicsVisualMaterializationSettings visualMaterializationSettings( + @Nonnull Store store, + @Nonnull Ref spaceRef) { + Store checkedStore = requireWorldThread(store, + "read PhysicsStore visual materialization settings"); + Ref checkedRef = Objects.requireNonNull(spaceRef, "spaceRef"); + if (!isSpaceRef(checkedStore, checkedRef)) { + return null; + } + PhysicsVisualMaterializationSettings settings = + new PhysicsVisualMaterializationSettings(); + VisualMaterializationSettingsComponent component = checkedStore.getComponent(checkedRef, + VisualMaterializationSettingsComponent.getComponentType()); + if (component != null) { + component.copyTo(settings); + } + return settings; + } + + @Nullable + public static PhysicsCollisionLodSettings collisionLodSettings( + @Nonnull Store store, + @Nonnull SpaceId spaceId) { + Ref ref = resolveRef(store, spaceId); + return ref != null ? collisionLodSettings(store, ref) : null; + } + + @Nullable + public static PhysicsCollisionLodSettings collisionLodSettings( + @Nonnull Store store, + @Nonnull Ref spaceRef) { + Store checkedStore = requireWorldThread(store, + "read PhysicsStore collision LOD settings"); + Ref checkedRef = Objects.requireNonNull(spaceRef, "spaceRef"); + if (!isSpaceRef(checkedStore, checkedRef)) { + return null; + } + PhysicsCollisionLodSettings settings = new PhysicsCollisionLodSettings(); + CollisionLodSettingsComponent component = checkedStore.getComponent(checkedRef, + CollisionLodSettingsComponent.getComponentType()); + if (component != null) { + component.copyTo(settings); + } + return settings; + } + + @Nullable + public static PhysicsExtensionSettings extensionSettings( + @Nonnull Store store, + @Nonnull SpaceId spaceId) { + Ref ref = resolveRef(store, spaceId); + return ref != null ? extensionSettings(store, ref) : null; + } + + @Nullable + public static PhysicsExtensionSettings extensionSettings( + @Nonnull Store store, + @Nonnull Ref spaceRef) { + Store checkedStore = requireWorldThread(store, + "read PhysicsStore extension settings"); + Ref checkedRef = Objects.requireNonNull(spaceRef, "spaceRef"); + if (!isSpaceRef(checkedStore, checkedRef)) { + return null; + } + PhysicsExtensionSettings settings = new PhysicsExtensionSettings(); + ExtensionSettingsComponent component = checkedStore.getComponent(checkedRef, + ExtensionSettingsComponent.getComponentType()); + if (component != null) { + component.copyTo(settings); + } + return settings; + } + + public static void putChunkCollisionSettings(@Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull PhysicsChunkCollisionSettings settings) { + Store checkedStore = requireWorldThread(store, + "update PhysicsStore chunk collision settings"); + PhysicsSpaceMutations.putChunkCollisionSettings(checkedStore, + Objects.requireNonNull(spaceId, "spaceId"), + Objects.requireNonNull(settings, "settings")); + } + + public static void putChunkCollisionSettings(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull PhysicsChunkCollisionSettings settings) { + Store checkedStore = requireWorldThread(store, + "update PhysicsStore chunk collision settings"); + PhysicsSpaceMutations.putChunkCollisionSettings(checkedStore, + Objects.requireNonNull(spaceRef, "spaceRef"), + Objects.requireNonNull(settings, "settings")); + } + + public static void putSolverSettings(@Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull PhysicsSolverSettings settings) { + Store checkedStore = requireWorldThread(store, + "update PhysicsStore solver settings"); + PhysicsSpaceMutations.putSolverSettings(checkedStore, + Objects.requireNonNull(spaceId, "spaceId"), + Objects.requireNonNull(settings, "settings")); + } + + public static void putSolverSettings(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull PhysicsSolverSettings settings) { + Store checkedStore = requireWorldThread(store, + "update PhysicsStore solver settings"); + PhysicsSpaceMutations.putSolverSettings(checkedStore, + Objects.requireNonNull(spaceRef, "spaceRef"), + Objects.requireNonNull(settings, "settings")); + } + + public static void putVisualSyncSettings(@Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull PhysicsVisualSyncSettings settings) { + Store checkedStore = requireWorldThread(store, + "update PhysicsStore visual sync settings"); + PhysicsSpaceMutations.putVisualSyncSettings(checkedStore, + Objects.requireNonNull(spaceId, "spaceId"), + Objects.requireNonNull(settings, "settings")); + } + + public static void putVisualSyncSettings(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull PhysicsVisualSyncSettings settings) { + Store checkedStore = requireWorldThread(store, + "update PhysicsStore visual sync settings"); + PhysicsSpaceMutations.putVisualSyncSettings(checkedStore, + Objects.requireNonNull(spaceRef, "spaceRef"), + Objects.requireNonNull(settings, "settings")); + } + + public static void putVisualMaterializationSettings(@Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull PhysicsVisualMaterializationSettings settings) { + Store checkedStore = requireWorldThread(store, + "update PhysicsStore visual materialization settings"); + PhysicsSpaceMutations.putVisualMaterializationSettings(checkedStore, + Objects.requireNonNull(spaceId, "spaceId"), + Objects.requireNonNull(settings, "settings")); + } + + public static void putVisualMaterializationSettings(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull PhysicsVisualMaterializationSettings settings) { + Store checkedStore = requireWorldThread(store, + "update PhysicsStore visual materialization settings"); + PhysicsSpaceMutations.putVisualMaterializationSettings(checkedStore, + Objects.requireNonNull(spaceRef, "spaceRef"), + Objects.requireNonNull(settings, "settings")); + } + + public static void putCollisionLodSettings(@Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull PhysicsCollisionLodSettings settings) { + Store checkedStore = requireWorldThread(store, + "update PhysicsStore collision LOD settings"); + PhysicsSpaceMutations.putCollisionLodSettings(checkedStore, + Objects.requireNonNull(spaceId, "spaceId"), + Objects.requireNonNull(settings, "settings")); + } + + public static void putCollisionLodSettings(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull PhysicsCollisionLodSettings settings) { + Store checkedStore = requireWorldThread(store, + "update PhysicsStore collision LOD settings"); + PhysicsSpaceMutations.putCollisionLodSettings(checkedStore, + Objects.requireNonNull(spaceRef, "spaceRef"), + Objects.requireNonNull(settings, "settings")); + } + + public static void putExtensionSettings(@Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull PhysicsExtensionSettings settings) { + Store checkedStore = requireWorldThread(store, + "update PhysicsStore extension settings"); + PhysicsSpaceMutations.putExtensionSettings(checkedStore, + Objects.requireNonNull(spaceId, "spaceId"), + Objects.requireNonNull(settings, "settings")); + } + + public static void putExtensionSettings(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull PhysicsExtensionSettings settings) { + Store checkedStore = requireWorldThread(store, + "update PhysicsStore extension settings"); + PhysicsSpaceMutations.putExtensionSettings(checkedStore, + Objects.requireNonNull(spaceRef, "spaceRef"), + Objects.requireNonNull(settings, "settings")); + } + + @Nullable + public static > T getSpaceComponent( + @Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull ComponentType componentType) { + Store checkedStore = requireWorldThread(store, + "read a PhysicsStore space component"); + Ref ref = requireSpaceRef(checkedStore, + Objects.requireNonNull(spaceId, "spaceId")); + return checkedStore.getComponent(ref, Objects.requireNonNull(componentType, + "componentType")); + } + + @Nullable + public static > T getSpaceComponent( + @Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull ComponentType componentType) { + Store checkedStore = requireWorldThread(store, + "read a PhysicsStore space component"); + Ref ref = requireSpaceRef(checkedStore, + Objects.requireNonNull(spaceRef, "spaceRef")); + return checkedStore.getComponent(ref, Objects.requireNonNull(componentType, + "componentType")); + } + + public static > void putSpaceComponent( + @Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull ComponentType componentType, + @Nonnull T component) { + Store checkedStore = requireWorldThread(store, + "put a PhysicsStore space component"); + putSpaceComponent(checkedStore, + requireSpaceRef(checkedStore, Objects.requireNonNull(spaceId, "spaceId")), + componentType, + component); + } + + public static > void putSpaceComponent( + @Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull ComponentType componentType, + @Nonnull T component) { + Store checkedStore = requireWorldThread(store, + "put a PhysicsStore space component"); + Ref ref = requireSpaceRef(checkedStore, + Objects.requireNonNull(spaceRef, "spaceRef")); + checkedStore.putComponent(ref, + Objects.requireNonNull(componentType, "componentType"), + copy(Objects.requireNonNull(component, "component"))); + checkedStore.getResource(PhysicsRuntimeResource.getResourceType()) + .markSpaceSettingsPending(ref); + } + + public static > boolean removeSpaceComponent( + @Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull ComponentType componentType) { + Store checkedStore = requireWorldThread(store, + "remove a PhysicsStore space component"); + return removeSpaceComponent(checkedStore, + requireSpaceRef(checkedStore, Objects.requireNonNull(spaceId, "spaceId")), + componentType); + } + + public static > boolean removeSpaceComponent( + @Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull ComponentType componentType) { + Store checkedStore = requireWorldThread(store, + "remove a PhysicsStore space component"); + Ref ref = requireSpaceRef(checkedStore, + Objects.requireNonNull(spaceRef, "spaceRef")); + boolean removed = checkedStore.removeComponentIfExists(ref, + Objects.requireNonNull(componentType, "componentType")); + if (removed) { + checkedStore.getResource(PhysicsRuntimeResource.getResourceType()) + .markSpaceSettingsPending(ref); + } + return removed; + } + + public static void removeEmpty(@Nonnull Store store, + @Nonnull SpaceId spaceId) { + Store checkedStore = requireWorldThread(store, + "remove an empty PhysicsStore space"); + PhysicsSpaceMutations.removeEmptySpace(checkedStore, + Objects.requireNonNull(spaceId, "spaceId")); + } + + public static void removeWithContents(@Nonnull Store store, + @Nonnull SpaceId spaceId) { + Store checkedStore = requireWorldThread(store, + "remove a PhysicsStore space with contents"); + PhysicsTopologyMutations.removeSpaceWithContents(checkedStore, + PhysicsSpaceMutations.requireSpaceUuid(checkedStore, + Objects.requireNonNull(spaceId, "spaceId"))); + } + + @Nonnull + private static Store requireWorldThread(@Nonnull Store store, + @Nonnull String operation) { + Store checkedStore = Objects.requireNonNull(store, "store"); + PhysicsThreading.requireWorldThread(checkedStore, operation); + return checkedStore; + } + + private static boolean isSpaceRef(@Nonnull Store store, + @Nonnull Ref ref) { + return ref.getStore() == store + && ref.isValid() + && store.getComponent(ref, SpaceComponent.getComponentType()) != null; + } + + @Nonnull + private static Ref requireSpaceRef(@Nonnull Store store, + @Nonnull SpaceId spaceId) { + Ref ref = resolveRef(store, spaceId); + if (ref == null) { + throw new IllegalArgumentException("PhysicsStore space id=" + spaceId.value() + + " is not registered"); + } + return ref; + } + + @Nonnull + private static Ref requireSpaceRef(@Nonnull Store store, + @Nonnull Ref ref) { + if (ref.getStore() != store || !ref.isValid()) { + throw new IllegalArgumentException("PhysicsStore space entity is not valid: " + ref); + } + if (store.getComponent(ref, SpaceComponent.getComponentType()) == null) { + throw new IllegalArgumentException("PhysicsStore entity is not a space entity: " + ref); + } + return ref; + } + + @Nonnull + @SuppressWarnings("unchecked") + private static > T copy(@Nonnull T component) { + return (T) component.clone(); + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsThreading.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsThreading.java new file mode 100644 index 00000000..59dc9fe6 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsThreading.java @@ -0,0 +1,213 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.early.PhysicsStoreWorld; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsAsyncCompletions; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStepSchedulerResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStoreReadQueueResource; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import java.util.function.Function; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +/** + * Thread assertions for direct PhysicsStore entity and backend access. + */ +public final class PhysicsThreading { + + private PhysicsThreading() { + } + + @Nonnull + public static World requireWorldThread(@Nonnull Store store, + @Nonnull String operation) { + World world = world(store); + if (!world.isInThread()) { + throw new IllegalStateException("Cannot " + operation + + " outside the owning PhysicsStore world thread"); + } + return world; + } + + public static void requireBackendIdle(@Nonnull Store store, + @Nonnull String operation) { + requireWorldThread(store, operation); + PhysicsStepSchedulerResource scheduler = store.getResource( + PhysicsStepSchedulerResource.getResourceType()); + if (scheduler.isStepPending()) { + throw new IllegalStateException("Cannot " + operation + + " while a PhysicsStore owner-lane step is pending"); + } + } + + @Nonnull + public static World world(@Nonnull Store store) { + return Objects.requireNonNull(store, "store").getExternalData().getWorld(); + } + + @Nonnull + public static CompletionStage executeOnWorldThread(@Nonnull World world, + @Nonnull String operation, + @Nonnull Consumer> mutation) { + Objects.requireNonNull(world, "world"); + Objects.requireNonNull(operation, "operation"); + Objects.requireNonNull(mutation, "mutation"); + CompletableFuture completion = new CompletableFuture<>(); + Runnable task = () -> execute(world, operation, mutation, completion); + try { + if (world.isInThread()) { + task.run(); + } else { + world.execute(task); + } + } catch (RuntimeException exception) { + PhysicsAsyncCompletions.fail(completion, exception); + } + return completion.minimalCompletionStage(); + } + + @Nonnull + public static CompletionStage enqueueReadOnWorldThread( + @Nonnull Store store, + @Nonnull String operation, + @Nonnull Function, R> read) { + return enqueueReadOnWorldThread(world(store), operation, read); + } + + @Nonnull + public static CompletionStage enqueueReadOnWorldThread(@Nonnull World world, + @Nonnull String operation, + @Nonnull Function, R> read) { + Objects.requireNonNull(world, "world"); + Objects.requireNonNull(operation, "operation"); + Objects.requireNonNull(read, "read"); + CompletableFuture completion = new CompletableFuture<>(); + Runnable task = () -> enqueueRead(world, operation, read, completion); + try { + if (world.isInThread()) { + task.run(); + } else { + world.execute(task); + } + } catch (RuntimeException exception) { + PhysicsAsyncCompletions.fail(completion, exception); + } + return completion.minimalCompletionStage(); + } + + @Nonnull + public static CompletionStage callWhenBackendIdleOnWorldThread(@Nonnull World world, + @Nonnull String operation, + @Nonnull Function, R> action) { + Objects.requireNonNull(world, "world"); + Objects.requireNonNull(operation, "operation"); + Objects.requireNonNull(action, "action"); + CompletableFuture completion = new CompletableFuture<>(); + Runnable task = () -> callWhenBackendIdle(world, operation, action, completion); + try { + if (world.isInThread()) { + task.run(); + } else { + world.execute(task); + } + } catch (RuntimeException exception) { + PhysicsAsyncCompletions.fail(completion, exception); + } + return completion.minimalCompletionStage(); + } + + private static void execute(@Nonnull World world, + @Nonnull String operation, + @Nonnull Consumer> mutation, + @Nonnull CompletableFuture completion) { + try { + Store store = store(world); + requireWorldThread(store, operation); + mutation.accept(store); + PhysicsAsyncCompletions.complete(completion, null); + } catch (RuntimeException | Error throwable) { + PhysicsAsyncCompletions.fail(completion, throwable); + } + } + + private static void callWhenBackendIdle(@Nonnull World world, + @Nonnull String operation, + @Nonnull Function, R> action, + @Nonnull CompletableFuture completion) { + try { + Store store = store(world); + requireWorldThread(store, operation); + PhysicsStepSchedulerResource scheduler = store.getResource( + PhysicsStepSchedulerResource.getResourceType()); + if (scheduler.isStepPending()) { + scheduler.whenIdle() + .whenComplete((_, failure) -> rescheduleBackendIdleCall(world, + operation, + action, + completion, + failure)); + return; + } + PhysicsAsyncCompletions.complete(completion, action.apply(store)); + } catch (RuntimeException | Error throwable) { + PhysicsAsyncCompletions.fail(completion, throwable); + } + } + + private static void rescheduleBackendIdleCall(@Nonnull World world, + @Nonnull String operation, + @Nonnull Function, R> action, + @Nonnull CompletableFuture completion, + Throwable failure) { + if (failure != null) { + PhysicsAsyncCompletions.fail(completion, failure); + return; + } + try { + world.execute(() -> callWhenBackendIdle(world, operation, action, completion)); + } catch (RuntimeException exception) { + PhysicsAsyncCompletions.fail(completion, exception); + } + } + + private static void enqueueRead(@Nonnull World world, + @Nonnull String operation, + @Nonnull Function, R> read, + @Nonnull CompletableFuture completion) { + try { + Store store = store(world); + requireWorldThread(store, operation); + store.getResource(PhysicsStoreReadQueueResource.getResourceType()) + .enqueueRead(read) + .whenComplete((value, failure) -> { + if (failure != null) { + PhysicsAsyncCompletions.fail(completion, failure); + } else { + PhysicsAsyncCompletions.complete(completion, value); + } + }); + } catch (RuntimeException | Error throwable) { + PhysicsAsyncCompletions.fail(completion, throwable); + } + } + + @Nonnull + public static Store store(@Nonnull World world) { + return ((PhysicsStoreWorld) Objects.requireNonNull(world, "world")).getPhysicsStore() + .getStore(); + } + + @Nullable + public static Store storeOrNull(@Nonnull World world) { + if (!(Objects.requireNonNull(world, "world") instanceof PhysicsStoreWorld physicsStoreWorld)) { + return null; + } + Store store = physicsStoreWorld.getPhysicsStore().getStore(); + return store.isShutdown() ? null : store; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsWorlds.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsWorlds.java new file mode 100644 index 00000000..4421de48 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsWorlds.java @@ -0,0 +1,117 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsEventResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldSettingsResource; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletionStage; +import javax.annotation.Nonnull; + +/** + * Public world-level reads and settings writes for PhysicsStore resources. + */ +public final class PhysicsWorlds { + + private PhysicsWorlds() { + } + + /** + * @deprecated Physics event plugin API is deprecated without replacement. + */ + @Deprecated(since = "0.1.0", forRemoval = false) + @Nonnull + public static PhysicsEventFrame latestEventFrame(@Nonnull Store store) { + Store checkedStore = requireWorldThread(store, + "read the latest PhysicsStore event frame"); + return checkedStore.getResource(PhysicsEventResource.getResourceType()).getLatestFrame(); + } + + /** + * @deprecated Physics event plugin API is deprecated without replacement. + */ + @Deprecated(since = "0.1.0", forRemoval = false) + @Nonnull + public static CompletionStage latestEventFrameAsync( + @Nonnull World world) { + return PhysicsThreading.enqueueReadOnWorldThread(world, + "queue PhysicsStore event frame read", + PhysicsWorlds::latestEventFrame); + } + + @Nonnull + public static PhysicsWorldSettings settings(@Nonnull Store store) { + Store checkedStore = requireWorldThread(store, + "read PhysicsStore world settings"); + return checkedStore.getResource(PhysicsWorldSettingsResource.getResourceType()) + .getSettings(); + } + + @Nonnull + public static CompletionStage settingsAsync(@Nonnull World world) { + return PhysicsThreading.enqueueReadOnWorldThread(world, + "queue PhysicsStore world settings read", + PhysicsWorlds::settings); + } + + public static void putSettings(@Nonnull Store store, + @Nonnull PhysicsWorldSettings settings) { + Store checkedStore = requireWorldThread(store, + "update PhysicsStore world settings"); + PhysicsThreading.requireBackendIdle(checkedStore, "update PhysicsStore world settings"); + PhysicsWorldSettings requested = new PhysicsWorldSettings( + Objects.requireNonNull(settings, "settings")); + validateStepModeSupported(checkedStore, requested.getStepMode()); + checkedStore.getResource(PhysicsWorldSettingsResource.getResourceType()) + .setSettings(requested); + } + + @Nonnull + public static CompletionStage putSettingsAsync(@Nonnull World world, + @Nonnull PhysicsWorldSettings settings) { + PhysicsWorldSettings requested = new PhysicsWorldSettings( + Objects.requireNonNull(settings, "settings")); + return PhysicsThreading.callWhenBackendIdleOnWorldThread(world, + "queue PhysicsStore world settings update", + store -> { + putSettings(store, requested); + return null; + }); + } + + private static void validateStepModeSupported(@Nonnull Store store, + @Nonnull PhysicsStepMode stepMode) { + if (stepMode != PhysicsStepMode.CCD) { + return; + } + List unsupportedSpaces = new ArrayList<>(); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + runtime.forEachRuntimeSpaceBinding((spaceRef, backendId, spaceHandle, backendRuntime) -> { + if (!backendRuntime.supportsContinuousCollision(spaceHandle.value())) { + UUID spaceUuid = runtime.getSpaceUuid(spaceRef); + unsupportedSpaces.add((spaceUuid != null ? spaceUuid : spaceRef) + + " backend=" + backendId.value()); + } + }); + if (!unsupportedSpaces.isEmpty()) { + throw new IllegalArgumentException("CCD step mode is not supported by PhysicsStore " + + "spaces: " + unsupportedSpaces); + } + } + + @Nonnull + private static Store requireWorldThread(@Nonnull Store store, + @Nonnull String operation) { + Store checkedStore = Objects.requireNonNull(store, "store"); + PhysicsThreading.requireWorldThread(checkedStore, operation); + return checkedStore; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/RaycastClosestBatchResult.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/RaycastClosestBatchResult.java similarity index 75% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/RaycastClosestBatchResult.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/RaycastClosestBatchResult.java index 44f38f8f..c90c9159 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/RaycastClosestBatchResult.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/RaycastClosestBatchResult.java @@ -1,7 +1,5 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; +package dev.hytalemodding.impulse.core.plugin.physics; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RaycastClosestBatchQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.view.RaycastHitView; import java.util.Arrays; import java.util.Objects; @@ -11,8 +9,8 @@ /** * Compact copied result for a batch of closest-raycast queries. * - *

Each result slot corresponds to the same index in the submitted - * {@link RaycastClosestBatchQuery#rays()} list. A {@code null} slot means that ray had no hit.

+ *

Each result slot corresponds to the same submitted ray index. A {@code null} slot means that + * ray had no hit.

*/ public final class RaycastClosestBatchResult { diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/RaycastHitView.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/RaycastHitView.java new file mode 100644 index 00000000..914a527f --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/RaycastHitView.java @@ -0,0 +1,100 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3f; + +/** + * Copied raycast geometry plus the PhysicsStore body ref hit by the backend. + */ +public record RaycastHitView(@Nullable Ref bodyRef, + float pointX, + float pointY, + float pointZ, + float normalX, + float normalY, + float normalZ, + float fraction, + float distance) { + + public RaycastHitView(@Nullable Ref bodyRef, + @Nonnull Vector3f point, + @Nonnull Vector3f normal, + float fraction, + float distance) { + this(bodyRef, + Objects.requireNonNull(point, "point").x, + point.y, + point.z, + Objects.requireNonNull(normal, "normal").x, + normal.y, + normal.z, + fraction, + distance); + } + + public RaycastHitView { + } + + @Nonnull + public PhysicsBodyType bodyType() { + DynamicsComponent dynamics = dynamicsComponent(bodyRef); + return dynamics != null ? dynamics.getBodyType() : PhysicsBodyType.STATIC; + } + + @Nonnull + public ShapeType shapeType() { + ShapeComponent shape = shapeComponent(bodyRef); + return shape != null ? shape.getShapeType() : ShapeType.UNKNOWN; + } + + @Nonnull + public Vector3f point() { + return new Vector3f(pointX, pointY, pointZ); + } + + @Nonnull + public Vector3f normal() { + return new Vector3f(normalX, normalY, normalZ); + } + + @Nonnull + public Vector3f copyPointTo(@Nonnull Vector3f target) { + return Objects.requireNonNull(target, "target").set(pointX, pointY, pointZ); + } + + @Nonnull + public Vector3f copyNormalTo(@Nonnull Vector3f target) { + return Objects.requireNonNull(target, "target").set(normalX, normalY, normalZ); + } + + @Nullable + private static DynamicsComponent dynamicsComponent(@Nullable Ref bodyRef) { + if (bodyRef == null || !bodyRef.isValid()) { + return null; + } + Store store = bodyRef.getStore(); + return store != null + ? store.getComponentConcurrent(bodyRef, DynamicsComponent.getComponentType()) + : null; + } + + @Nullable + private static ShapeComponent shapeComponent(@Nullable Ref bodyRef) { + if (bodyRef == null || !bodyRef.isValid()) { + return null; + } + Store store = bodyRef.getStore(); + return store != null + ? store.getComponentConcurrent(bodyRef, ShapeComponent.getComponentType()) + : null; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/RaycastSegment.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/RaycastSegment.java similarity index 97% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/RaycastSegment.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/RaycastSegment.java index 3c6ed384..0890081d 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/RaycastSegment.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/RaycastSegment.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; +package dev.hytalemodding.impulse.core.plugin.physics; import java.util.Objects; import javax.annotation.Nonnull; diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/RigidBodySpawnSettings.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/RigidBodySpawnSettings.java similarity index 99% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/RigidBodySpawnSettings.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/RigidBodySpawnSettings.java index 629b856c..ff605d08 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/RigidBodySpawnSettings.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/RigidBodySpawnSettings.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; +package dev.hytalemodding.impulse.core.plugin.physics; import javax.annotation.Nonnull; import javax.annotation.Nullable; diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/SolverCapabilitySummary.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/SolverCapabilitySummary.java similarity index 91% rename from impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/SolverCapabilitySummary.java rename to impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/SolverCapabilitySummary.java index 0835cecc..fd072b6b 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/SolverCapabilitySummary.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/SolverCapabilitySummary.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; +package dev.hytalemodding.impulse.core.plugin.physics; import dev.hytalemodding.impulse.api.SpaceId; import java.util.Objects; diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/SpaceSummary.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/SpaceSummary.java new file mode 100644 index 00000000..76db1d8a --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/physics/SpaceSummary.java @@ -0,0 +1,78 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.SpaceId; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * Copied store tick lane space diagnostics. + */ +public record SpaceSummary(@Nonnull SpaceId spaceId, + @Nonnull BackendId backendId, + int bodyCount, + int jointCount, + boolean runtimeStatsAvailable, + int runtimeBodyCount, + int runtimeColliderCount, + int runtimeActiveBodyCount, + int runtimeContactPairCount, + int runtimeContactManifoldCount, + int runtimeContactPointCount, + int runtimeDynamicDynamicContactPairCount, + int runtimeTerrainContactPairCount, + int runtimeActiveIslandCount, + int runtimeJointCount) { + + public SpaceSummary(@Nonnull SpaceId spaceId, + @Nonnull BackendId backendId, + int bodyCount, + int jointCount) { + this(spaceId, + backendId, + bodyCount, + jointCount, + false, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0); + } + + public SpaceSummary { + Objects.requireNonNull(spaceId, "spaceId"); + Objects.requireNonNull(backendId, "backendId"); + bodyCount = Math.max(0, bodyCount); + jointCount = Math.max(0, jointCount); + if (!runtimeStatsAvailable) { + runtimeBodyCount = 0; + runtimeColliderCount = 0; + runtimeActiveBodyCount = 0; + runtimeContactPairCount = 0; + runtimeContactManifoldCount = 0; + runtimeContactPointCount = 0; + runtimeDynamicDynamicContactPairCount = 0; + runtimeTerrainContactPairCount = 0; + runtimeActiveIslandCount = 0; + runtimeJointCount = 0; + } else { + runtimeBodyCount = Math.max(0, runtimeBodyCount); + runtimeColliderCount = Math.max(0, runtimeColliderCount); + runtimeActiveBodyCount = Math.max(0, runtimeActiveBodyCount); + runtimeContactPairCount = Math.max(0, runtimeContactPairCount); + runtimeContactManifoldCount = Math.max(0, runtimeContactManifoldCount); + runtimeContactPointCount = Math.max(0, runtimeContactPointCount); + runtimeDynamicDynamicContactPairCount = + Math.max(0, runtimeDynamicDynamicContactPairCount); + runtimeTerrainContactPairCount = Math.max(0, runtimeTerrainContactPairCount); + runtimeActiveIslandCount = Math.max(0, runtimeActiveIslandCount); + runtimeJointCount = Math.max(0, runtimeJointCount); + } + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/resources/PhysicsMutationHandle.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/resources/PhysicsMutationHandle.java deleted file mode 100644 index 14c2d117..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/resources/PhysicsMutationHandle.java +++ /dev/null @@ -1,127 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.resources; - -import java.util.Objects; -import java.util.concurrent.CancellationException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionException; -import java.util.concurrent.CompletionStage; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Public completion handle for an asynchronous physics resource mutation. - * - * @param reserved logical value returned to the caller before the mutation runs - */ -public final class PhysicsMutationHandle { - - @Nonnull - private final String operation; - @Nullable - private final T value; - @Nonnull - private final CompletableFuture completion; - - private PhysicsMutationHandle(@Nonnull String operation, - @Nullable T value, - @Nonnull CompletableFuture completion) { - this.operation = Objects.requireNonNull(operation, "operation"); - this.value = value; - this.completion = Objects.requireNonNull(completion, "completion"); - } - - @Nonnull - public static PhysicsMutationHandle completed(@Nonnull String operation, - @Nullable T value) { - return new PhysicsMutationHandle<>(operation, value, CompletableFuture.completedFuture(value)); - } - - @Nonnull - public static PhysicsMutationHandle failed(@Nonnull String operation, - @Nullable T value, - @Nonnull Throwable failure) { - CompletableFuture completion = new CompletableFuture<>(); - completion.completeExceptionally(Objects.requireNonNull(failure, "failure")); - return new PhysicsMutationHandle<>(operation, value, completion); - } - - @Nonnull - public static PhysicsMutationHandle fromCompletion(@Nonnull String operation, - @Nullable T value, - @Nonnull CompletionStage source) { - CompletableFuture completion = Objects.requireNonNull(source, "source") - .thenApply(_ -> value) - .toCompletableFuture(); - return new PhysicsMutationHandle<>(operation, value, completion); - } - - @Nonnull - public String operation() { - return operation; - } - - @Nullable - public T value() { - return value; - } - - @Nonnull - public CompletionStage completion() { - return completion.minimalCompletionStage(); - } - - public boolean isDone() { - return completion.isDone(); - } - - public boolean completedSuccessfully() { - return completion.isDone() - && !completion.isCompletedExceptionally() - && !completion.isCancelled(); - } - - public boolean failed() { - return failure() != null; - } - - @Nullable - public Throwable failure() { - if (!completion.isDone()) { - return null; - } - try { - completion.join(); - return null; - } catch (CompletionException exception) { - return unwrap(exception); - } catch (CancellationException exception) { - return exception; - } - } - - @Nullable - public T join() { - return completion.join(); - } - - public void throwIfFailed() { - Throwable failure = failure(); - switch (failure) { - case null -> { - return; - } - case RuntimeException runtimeException -> throw runtimeException; - case Error error -> throw error; - default -> { - } - } - throw new IllegalStateException("Async physics mutation " + operation + " failed", - failure); - } - - @Nonnull - private static Throwable unwrap(@Nonnull CompletionException exception) { - Throwable cause = exception.getCause(); - return cause != null ? cause : exception; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/resources/PhysicsWorldResource.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/resources/PhysicsWorldResource.java deleted file mode 100644 index 4f014491..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/resources/PhysicsWorldResource.java +++ /dev/null @@ -1,407 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.resources; - -import com.hypixel.hytale.component.Resource; -import com.hypixel.hytale.component.ResourceType; -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyRegistrationView; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionBuildStats; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionPrewarmStats; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionStats; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandHandle; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandRecipe; -import dev.hytalemodding.impulse.core.plugin.simulation.query.PhysicsQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.query.PhysicsQueryHandle; -import dev.hytalemodding.impulse.core.plugin.snapshot.PhysicsBodySnapshotEntry; -import java.util.Collection; -import java.util.function.Consumer; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3d; -import org.joml.Vector3f; - -/** - * Public alpha facade for a world's physics runtime resource. - * - *

The concrete Impulse runtime lives in the internal package. Plugin-facing code should depend on - * this facade for explicit space lifecycle, world settings, command submission, body lifetime by key, - * immutable snapshots, read-only registration views, public attachment/control hooks, and world - * collision operations.

- * - *

No physics space is created implicitly. Consumers choose which explicit {@link SpaceId} to - * target for each operation.

- * - *

This facade does not directly return live backend spaces or bodies. Gameplay code should use - * physics simulation commands, copied queries, and published snapshots. Advanced diagnostics should - * be modeled as backend-neutral commands or copied queries instead of retaining live backend - * handles.

- */ -public abstract class PhysicsWorldResource implements Resource { - - protected PhysicsWorldResource() { - } - - /** - * Records copied simulation intent through the fluent command DSL and submits it. - * - *

The returned handle completes when the physics owner executes the batch. Snapshot - * publication and ECS visual synchronization are separate phases and can lag behind command - * completion.

- */ - @Nonnull - public abstract PhysicsCommandHandle submitCommands(long submittedServerTick, - @Nonnull PhysicsCommandRecipe recipe); - - /** - * Records copied simulation intent through the fluent command DSL with a capacity hint. - * - *

{@code expectedOperations} sizes the internal recorder arrays. It is a performance hint, - * not a correctness requirement.

- */ - @Nonnull - public abstract PhysicsCommandHandle submitCommands(long submittedServerTick, - int expectedOperations, - @Nonnull PhysicsCommandRecipe recipe); - - /** - * Runs a copied owner-lane physics query without exposing live backend handles. - * - *

Queries should return immutable or defensively copied values. Use commands for mutations - * so all writes stay ordered through the physics owner.

- */ - @Nonnull - public abstract PhysicsQueryHandle query(@Nonnull PhysicsQuery query); - - /** - * Returns the latest value-only physics owner event frame. - * - *

Event frames describe owner-lane outcomes. They do not expose live - * backend handles and do not imply that command completion has been - * included in a captured or reader-applied body snapshot.

- */ - @Nonnull - public abstract PhysicsEventFrame getLatestEventFrame(); - - /** - * Returns a defensive copy of the world-level simulation settings. - * - *

Changing the returned copy has no effect until it is passed to - * {@link #setWorldSettings(PhysicsWorldSettings)} or - * {@link #setWorldSettingsAsync(PhysicsWorldSettings)}.

- */ - @Nonnull - public abstract PhysicsWorldSettings getWorldSettings(); - - /** - * Applies world-level simulation settings on the physics owner lane. - */ - public abstract void setWorldSettings(@Nonnull PhysicsWorldSettings settings); - - /** - * Queues a world-level simulation settings update. - */ - @Nonnull - public abstract PhysicsMutationHandle setWorldSettingsAsync( - @Nonnull PhysicsWorldSettings settings); - - /** - * Creates a physics space using default settings and returns its id. - * - *

Creation is serialized through this world's logical physics owner lane. Callers must not - * infer a stable Java thread identity from the synchronous return path.

- */ - @Nonnull - public abstract SpaceId createSpace(@Nonnull BackendId backendId); - - /** - * Creates a physics space for logging under the supplied world name and returns its id. - * - *

No default space is created implicitly; the returned id is the explicit space handle for - * later world-resource operations.

- */ - @Nonnull - public abstract SpaceId createSpace(@Nonnull BackendId backendId, - @Nonnull String worldName); - - /** - * Creates a physics space with generated logical id and supplied settings. - * - *

The live backend space is created inside the serialized owner lane. Use the async variant - * when the caller should not block on owner-lane execution.

- */ - @Nonnull - public abstract SpaceId createSpace(@Nonnull BackendId backendId, - @Nonnull String worldName, - @Nonnull PhysicsSpaceSettings settings); - - /** - * Creates a physics space with an explicit logical id and supplied settings. - * - *

The explicit id is reserved by the caller, but live backend creation still runs inside the - * serialized owner lane.

- */ - @Nonnull - public abstract SpaceId createSpace(@Nonnull BackendId backendId, - @Nonnull SpaceId spaceId, - @Nonnull String worldName, - @Nonnull PhysicsSpaceSettings settings); - - /** - * Queues physics-space creation and returns the reserved generated space id. - * - *

The returned mutation handle completes when the owner lane creates the live backend - * space, not when a later snapshot or ECS reader has consumed any resulting state.

- */ - @Nonnull - public abstract PhysicsMutationHandle createSpaceAsync( - @Nonnull BackendId backendId, - @Nonnull String worldName, - @Nonnull PhysicsSpaceSettings settings); - - /** - * Queues physics-space creation and returns the requested explicit space id. - * - *

Different worlds may queue work concurrently, but this world's spaces remain serialized by - * its owner lane.

- */ - @Nonnull - public abstract PhysicsMutationHandle createSpaceAsync( - @Nonnull BackendId backendId, - @Nonnull SpaceId spaceId, - @Nonnull String worldName, - @Nonnull PhysicsSpaceSettings settings); - - /** - * Returns whether a physics space id is currently registered. - */ - public abstract boolean hasSpace(@Nonnull SpaceId spaceId); - - /** - * Returns a snapshot collection of registered physics space ids. - */ - @Nonnull - public abstract Collection getSpaceIds(); - - /** - * Returns the number of registered physics spaces. - */ - public abstract int getSpaceCount(); - - /** - * Captures and publishes body snapshots from the live backend state. - * - * @return number of published body snapshots - */ - public abstract int refreshBodySnapshots(); - - /** - * Returns the latest published snapshot for a body, capturing a copied live snapshot on the - * physics owner if the body is registered but missing from the published frame. The fallback - * snapshot is not published into the reader-side frame. - */ - @Nonnull - public abstract PhysicsBodySnapshot getBodySnapshot(@Nonnull RigidBodyKey bodyKey); - - /** - * Returns the number of body snapshots in the latest published frame. - */ - public abstract int getBodySnapshotCount(); - - /** - * Returns the number of body snapshots in the latest published frame for one space. - */ - public abstract int getBodySnapshotCount(@Nonnull SpaceId spaceId); - - /** - * Returns the number of occupied snapshot broad-phase cells. - */ - public abstract int getBodySnapshotCellCount(); - - /** - * Rebuilds world collision around a center for the requested space. - */ - @Nonnull - public abstract WorldCollisionBuildStats rebuildWorldCollisionAround(@Nonnull World world, - @Nonnull SpaceId spaceId, - @Nonnull Vector3d center, - int radius); - - /** - * Rebuilds cached world-collision sections around a center for the requested space without - * clearing retained terrain outside that radius. - */ - @Nonnull - public abstract WorldCollisionBuildStats refreshWorldCollisionAround(@Nonnull World world, - @Nonnull SpaceId spaceId, - @Nonnull Vector3d center, - int radius); - - /** - * Ensures world collision exists around one or more centers for the requested space. - */ - @Nonnull - public abstract WorldCollisionPrewarmStats ensureWorldCollisionAround(@Nonnull World world, - @Nonnull SpaceId spaceId, - @Nonnull Iterable centers, - int radius, - long tick); - - /** - * Clears cached world collision for the requested space. - */ - public abstract int clearWorldCollision(@Nonnull SpaceId spaceId); - - /** - * Returns world-collision runtime statistics. - */ - @Nonnull - public abstract WorldCollisionStats getWorldCollisionStats(); - - /** - * Iterates published body snapshots for one space. - */ - public abstract void forEachBodySnapshot(@Nonnull SpaceId spaceId, - @Nonnull Consumer consumer); - - /** - * Iterates published body snapshots near a point. - * - * @return number of matching snapshot entries - */ - public abstract int forEachBodySnapshotNear(@Nonnull SpaceId spaceId, - @Nonnull Vector3f center, - float radius, - @Nonnull Consumer consumer); - - /** - * Removes a physics space and destroys its registered bodies. - */ - public abstract void removeSpace(@Nonnull SpaceId spaceId); - - /** - * Removes a physics space and destroys its registered bodies, using the world name for logging. - */ - public abstract void removeSpace(@Nonnull SpaceId spaceId, @Nonnull String worldName); - - /** - * Queues physics-space removal and returns the removed space id. - */ - @Nonnull - public abstract PhysicsMutationHandle removeSpaceAsync(@Nonnull SpaceId spaceId, - @Nonnull String worldName); - - /** - * Removes all physics spaces and destroys their registered bodies. - */ - public abstract void clearAllSpaces(@Nonnull String worldName); - - /** - * Queues removal of all physics spaces. - */ - @Nonnull - public abstract PhysicsMutationHandle clearAllSpacesAsync(@Nonnull String worldName); - - /** - * Returns the current settings for a registered physics space. - */ - @Nonnull - public abstract PhysicsSpaceSettings getSpaceSettings(@Nonnull SpaceId spaceId); - - /** - * Applies settings to a registered physics space on the physics owner lane. - */ - public abstract void setSpaceSettings(@Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceSettings settings); - - /** - * Queues settings replacement for a registered physics space. - */ - @Nonnull - public abstract PhysicsMutationHandle setSpaceSettingsAsync(@Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceSettings settings); - - /** - * Destroys a registered body by stable key and removes it from its physics space. - */ - public abstract void destroyBody(@Nonnull RigidBodyKey bodyKey); - - /** - * Queues destruction of a registered body by stable key. - */ - @Nonnull - public abstract PhysicsMutationHandle destroyBodyAsync( - @Nonnull RigidBodyKey bodyKey); - - /** - * Returns immutable registration metadata for a body key. - */ - @Nullable - public abstract PhysicsBodyRegistrationView getBodyRegistrationView( - @Nonnull RigidBodyKey bodyKey); - - /** - * Returns immutable registration metadata for every registered body. - */ - @Nonnull - public abstract Collection getBodyRegistrationViews(); - - /** - * Returns the number of registered bodies. - */ - public abstract int getBodyRegistrationCount(); - - /** - * Returns the number of registered bodies with a persistence mode. - */ - public abstract int getBodyRegistrationCount( - @Nonnull PhysicsBodyPersistenceMode persistenceMode); - - /** - * Returns immutable registration metadata for bodies of a kind. - */ - @Nonnull - public abstract Collection getBodyRegistrationViews( - @Nonnull PhysicsBodyKind kind); - - /** - * Returns ECS attachments associated with a registered body key. - */ - @Nonnull - public abstract Collection> getBodyAttachments(@Nonnull RigidBodyKey bodyKey); - - /** - * Returns whether a registered body has one or more ECS attachments without materializing the - * attachment collection. - */ - public abstract boolean hasBodyAttachments(@Nonnull RigidBodyKey bodyKey); - - /** - * Destroys all registered bodies while preserving registered physics spaces. - */ - public abstract void clearBodies(); - - /** - * Queues destruction of all registered bodies while preserving registered physics spaces. - */ - @Nonnull - public abstract PhysicsMutationHandle clearBodiesAsync(); - - public static ResourceType getResourceType() { - return ImpulsePlugin.get().getPhysicsWorldResourceType(); - } - - @Nonnull - @Override - public abstract PhysicsWorldResource clone(); -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsSolverSettings.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsSolverSettings.java index d15f23ed..636e5d49 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsSolverSettings.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsSolverSettings.java @@ -1,10 +1,12 @@ package dev.hytalemodding.impulse.core.plugin.settings; +import lombok.Getter; import javax.annotation.Nonnull; /** * Portable solver and activation tuning for a physics space. */ +@Getter public class PhysicsSolverSettings { /** @@ -68,10 +70,6 @@ public PhysicsSolverSettings(@Nonnull PhysicsSolverSettings settings) { dynamicSleepTimeUntilSleep = settings.dynamicSleepTimeUntilSleep; } - public int getSolverIterations() { - return solverIterations; - } - public void setSolverIterations(int solverIterations) { if (solverIterations < 1) { throw new IllegalArgumentException("Solver iterations must be positive"); @@ -79,10 +77,6 @@ public void setSolverIterations(int solverIterations) { this.solverIterations = solverIterations; } - public int getStabilizationIterations() { - return stabilizationIterations; - } - public void setStabilizationIterations(int stabilizationIterations) { if (stabilizationIterations < 0) { throw new IllegalArgumentException("Stabilization iterations cannot be negative"); @@ -98,10 +92,6 @@ public void setDynamicSleepTuning(float linearThreshold, setDynamicSleepTimeUntilSleep(timeUntilSleep); } - public float getDynamicSleepLinearThreshold() { - return dynamicSleepLinearThreshold; - } - public void setDynamicSleepLinearThreshold(float dynamicSleepLinearThreshold) { this.dynamicSleepLinearThreshold = PhysicsSettingsValidation.requireFiniteAtLeast( "Dynamic sleep linear threshold", @@ -109,10 +99,6 @@ public void setDynamicSleepLinearThreshold(float dynamicSleepLinearThreshold) { 0.0f); } - public float getDynamicSleepAngularThreshold() { - return dynamicSleepAngularThreshold; - } - public void setDynamicSleepAngularThreshold(float dynamicSleepAngularThreshold) { this.dynamicSleepAngularThreshold = PhysicsSettingsValidation.requireFiniteAtLeast( "Dynamic sleep angular threshold", @@ -120,10 +106,6 @@ public void setDynamicSleepAngularThreshold(float dynamicSleepAngularThreshold) 0.0f); } - public float getDynamicSleepTimeUntilSleep() { - return dynamicSleepTimeUntilSleep; - } - public void setDynamicSleepTimeUntilSleep(float dynamicSleepTimeUntilSleep) { this.dynamicSleepTimeUntilSleep = PhysicsSettingsValidation.requireFiniteAtLeast( "Dynamic sleep time", diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsSpaceSettings.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsSpaceSettings.java deleted file mode 100644 index 4a22fc59..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsSpaceSettings.java +++ /dev/null @@ -1,123 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.settings; - -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionMode; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import javax.annotation.Nonnull; - -/** - * Per-space configuration aggregate for terrain collision, solver tuning, - * collision LOD, visual sync, and detached visual materialization. - * - *

Settings are attached to a space at creation time via - * {@link PhysicsWorldResource#createSpace} and can be changed later with - * {@link PhysicsWorldResource#setSpaceSettings}.

- * - *

The grouped accessors expose the domain-owned settings objects. Internal - * code should read and mutate the domain group directly instead of adding flat - * shortcut state here.

- * - *

Default settings have world collision disabled ({@link WorldCollisionMode#NONE}), - * which keeps Impulse fully opt-in: no terrain bodies are created unless the integrator - * explicitly opts in.

- */ -public class PhysicsSpaceSettings { - - @Nonnull - private final PhysicsWorldCollisionSettings worldCollisionSettings; - @Nonnull - private final PhysicsVisualSyncSettings visualSyncSettings; - @Nonnull - private final PhysicsSolverSettings solverSettings; - @Nonnull - private final PhysicsVisualMaterializationSettings visualMaterializationSettings; - @Nonnull - private final PhysicsCollisionLodSettings collisionLodSettings; - @Nonnull - private final PhysicsExtensionSettings extensionSettings; - - public PhysicsSpaceSettings() { - worldCollisionSettings = new PhysicsWorldCollisionSettings(); - visualSyncSettings = new PhysicsVisualSyncSettings(); - solverSettings = new PhysicsSolverSettings(); - visualMaterializationSettings = new PhysicsVisualMaterializationSettings(); - collisionLodSettings = new PhysicsCollisionLodSettings(); - extensionSettings = new PhysicsExtensionSettings(); - } - - public PhysicsSpaceSettings(@Nonnull PhysicsSpaceSettings settings) { - worldCollisionSettings = - new PhysicsWorldCollisionSettings(settings.worldCollisionSettings); - visualSyncSettings = - new PhysicsVisualSyncSettings(settings.visualSyncSettings); - solverSettings = - new PhysicsSolverSettings(settings.solverSettings); - visualMaterializationSettings = - new PhysicsVisualMaterializationSettings(settings.visualMaterializationSettings); - collisionLodSettings = - new PhysicsCollisionLodSettings(settings.collisionLodSettings); - extensionSettings = new PhysicsExtensionSettings(settings.extensionSettings); - } - - /** - * Terrain collision streaming and chunk-boundary behavior. - */ - @Nonnull - public PhysicsWorldCollisionSettings getWorldCollisionSettings() { - return worldCollisionSettings; - } - - /** - * Server-to-Hytale transform sync sampling for entity-backed and follower visuals. - */ - @Nonnull - public PhysicsVisualSyncSettings getVisualSyncSettings() { - return visualSyncSettings; - } - - /** - * Backend solver and sleep tuning. - */ - @Nonnull - public PhysicsSolverSettings getSolverSettings() { - return solverSettings; - } - - /** - * Generated visual proxies for detached bodies. - */ - @Nonnull - public PhysicsVisualMaterializationSettings getVisualMaterializationSettings() { - return visualMaterializationSettings; - } - - /** - * Distance-based dynamic-body collision LOD. - */ - @Nonnull - public PhysicsCollisionLodSettings getCollisionLodSettings() { - return collisionLodSettings; - } - - /** - * Capability-keyed backend extension settings. - */ - @Nonnull - public PhysicsExtensionSettings getExtensionSettings() { - return extensionSettings; - } - - @Nonnull - public static PhysicsSpaceSettings defaults() { - return new PhysicsSpaceSettings(); - } - - /** - * Convenience factory for a space with streaming world collision enabled. - */ - @Nonnull - public static PhysicsSpaceSettings streamingWorldCollision() { - PhysicsSpaceSettings settings = new PhysicsSpaceSettings(); - settings.getWorldCollisionSettings().setWorldCollisionMode(WorldCollisionMode.STREAMING); - return settings; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsStepSchedulingMode.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsStepSchedulingMode.java index 275d20ac..6d921767 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsStepSchedulingMode.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsStepSchedulingMode.java @@ -5,16 +5,16 @@ /** * Controls how the world-level scheduler handles elapsed {@code dt} while a - * previous owner step is still unpublished. + * previous store tick step is still unpublished. */ public enum PhysicsStepSchedulingMode { /** - * Pending owner-lane steps do not add their {@code dt} to the next accepted step. + * Pending store tick steps and post-skip catch-up {@code dt} are dropped. */ DROP_PENDING_DT("drop_pending_dt"), /** - * Pending owner-lane steps accumulate elapsed {@code dt}; the next accepted step + * Pending store tick steps accumulate elapsed {@code dt}; the next accepted step * catches up once, bounded by the scheduler's hard cap. */ ACCUMULATE_PENDING_DT("accumulate_pending_dt"); @@ -34,7 +34,7 @@ public String getSerializedName() { @Nonnull public String describePendingStepBehavior() { return switch (this) { - case DROP_PENDING_DT -> "drop dt while an owner step is pending"; + case DROP_PENDING_DT -> "drop pending dt and prevent post-skip catch-up"; case ACCUMULATE_PENDING_DT -> "accumulate pending dt for one capped catch-up step"; }; } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsWorldCollisionSettings.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsWorldCollisionSettings.java deleted file mode 100644 index fd083d6d..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsWorldCollisionSettings.java +++ /dev/null @@ -1,197 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.settings; - -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionMode; -import lombok.Getter; -import lombok.Setter; -import javax.annotation.Nonnull; - -/** - * Terrain collision streaming settings for a physics space. - */ -public class PhysicsWorldCollisionSettings { - - /** - * Block radius around each tracked player for streaming collision. - */ - public static final int DEFAULT_WORLD_COLLISION_RADIUS = 8; - - /** - * Hard block-radius cap for player-centered world collision streaming. - */ - public static final int MAX_WORLD_COLLISION_RADIUS = 128; - - /** - * Block radius around each active dynamic physics body for streaming collision. - * Smaller than the player radius because bodies should not pull collision - * as far as players, but still need terrain to land on. - */ - public static final int DEFAULT_WORLD_COLLISION_BODY_RADIUS = 4; - - /** - * Hard block-radius cap for dynamic-body world collision streaming. - */ - public static final int MAX_WORLD_COLLISION_BODY_RADIUS = 64; - - /** - * Ticks before an unused section's collision bodies are pruned. - */ - public static final int DEFAULT_WORLD_COLLISION_TTL_TICKS = 100; - - /** - * Hard tick cap for retaining unused streamed collision sections. - */ - public static final int MAX_WORLD_COLLISION_TTL_TICKS = 12_000; - - /** - * Default behavior when an entity-backed body reaches an unloaded chunk border. - */ - @Nonnull - public static final EntityChunkBoundaryMode DEFAULT_ENTITY_CHUNK_BOUNDARY_MODE = - EntityChunkBoundaryMode.PAUSE_UNTIL_LOADED; - - /** - * Whether full-cube world sections should use native backend voxel terrain when available. - */ - public static final boolean DEFAULT_NATIVE_VOXEL_TERRAIN_ENABLED = false; - - /** - * Default friction applied to generated terrain collision bodies. - */ - public static final float DEFAULT_TERRAIN_FRICTION = 0.75f; - - /** - * Default restitution applied to generated terrain collision bodies. - */ - public static final float DEFAULT_TERRAIN_RESTITUTION = 0.0f; - - /** - * World collision mode for this space. Defaults to NONE so Impulse - * is fully opt-in: no terrain collision is created unless explicitly requested. - */ - @Nonnull - private WorldCollisionMode worldCollisionMode = WorldCollisionMode.NONE; - - /** - * How entity-backed bodies behave when they reach an unloaded chunk border. - */ - @Nonnull - private EntityChunkBoundaryMode entityChunkBoundaryMode = DEFAULT_ENTITY_CHUNK_BOUNDARY_MODE; - - /** - * Enables native backend voxel terrain for full-cube world collision. - */ - @Setter - @Getter - private boolean nativeVoxelTerrainEnabled = DEFAULT_NATIVE_VOXEL_TERRAIN_ENABLED; - - /** - * Block radius around tracked player positions for streaming or manual build. - */ - @Getter - private int worldCollisionRadius = DEFAULT_WORLD_COLLISION_RADIUS; - - /** - * Block radius around active dynamic physics bodies for streaming collision. - */ - @Getter - private int worldCollisionBodyRadius = DEFAULT_WORLD_COLLISION_BODY_RADIUS; - - /** - * How long a section stays loaded after its last use, in server ticks. - */ - @Getter - private int worldCollisionTtlTicks = DEFAULT_WORLD_COLLISION_TTL_TICKS; - - /** - * Friction applied to generated terrain bodies for this space. - */ - @Getter - private float terrainFriction = DEFAULT_TERRAIN_FRICTION; - - /** - * Restitution applied to generated terrain bodies for this space. - */ - @Getter - private float terrainRestitution = DEFAULT_TERRAIN_RESTITUTION; - - public PhysicsWorldCollisionSettings() { - } - - public PhysicsWorldCollisionSettings(@Nonnull PhysicsWorldCollisionSettings settings) { - worldCollisionMode = settings.worldCollisionMode; - entityChunkBoundaryMode = settings.entityChunkBoundaryMode; - nativeVoxelTerrainEnabled = settings.nativeVoxelTerrainEnabled; - worldCollisionRadius = settings.worldCollisionRadius; - worldCollisionBodyRadius = settings.worldCollisionBodyRadius; - worldCollisionTtlTicks = settings.worldCollisionTtlTicks; - terrainFriction = settings.terrainFriction; - terrainRestitution = settings.terrainRestitution; - } - - @Nonnull - public WorldCollisionMode getWorldCollisionMode() { - return worldCollisionMode; - } - - public void setWorldCollisionMode(@Nonnull WorldCollisionMode worldCollisionMode) { - this.worldCollisionMode = worldCollisionMode; - } - - @Nonnull - public EntityChunkBoundaryMode getEntityChunkBoundaryMode() { - return entityChunkBoundaryMode; - } - - public void setEntityChunkBoundaryMode( - @Nonnull EntityChunkBoundaryMode entityChunkBoundaryMode) { - this.entityChunkBoundaryMode = entityChunkBoundaryMode; - } - - public void setWorldCollisionRadius(int worldCollisionRadius) { - this.worldCollisionRadius = PhysicsSettingsValidation.requirePositiveAtMost( - "World collision radius", - worldCollisionRadius, - MAX_WORLD_COLLISION_RADIUS); - } - - public void setWorldCollisionBodyRadius(int worldCollisionBodyRadius) { - this.worldCollisionBodyRadius = PhysicsSettingsValidation.requirePositiveAtMost( - "World collision body radius", - worldCollisionBodyRadius, - MAX_WORLD_COLLISION_BODY_RADIUS); - } - - public void setWorldCollisionTtlTicks(int worldCollisionTtlTicks) { - this.worldCollisionTtlTicks = PhysicsSettingsValidation.requirePositiveAtMost( - "World collision TTL", - worldCollisionTtlTicks, - MAX_WORLD_COLLISION_TTL_TICKS); - } - - public void setTerrainFriction(float terrainFriction) { - this.terrainFriction = PhysicsSettingsValidation.requireFiniteAtLeast( - "Terrain friction", - terrainFriction, - 0.0f); - } - - public void setTerrainRestitution(float terrainRestitution) { - this.terrainRestitution = PhysicsSettingsValidation.requireFiniteAtLeast( - "Terrain restitution", - terrainRestitution, - 0.0f); - } - - public void setTerrainMaterial(float terrainFriction, float terrainRestitution) { - float validatedFriction = PhysicsSettingsValidation.requireFiniteAtLeast( - "Terrain friction", - terrainFriction, - 0.0f); - float validatedRestitution = PhysicsSettingsValidation.requireFiniteAtLeast( - "Terrain restitution", - terrainRestitution, - 0.0f); - this.terrainFriction = validatedFriction; - this.terrainRestitution = validatedRestitution; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsWorldSettings.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsWorldSettings.java index bd8672d7..27c86d08 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsWorldSettings.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/settings/PhysicsWorldSettings.java @@ -1,5 +1,6 @@ package dev.hytalemodding.impulse.core.plugin.settings; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventCollectionMode; import lombok.Getter; import java.util.Objects; import javax.annotation.Nonnull; @@ -9,7 +10,7 @@ * *

These values control how the world schedules and subdivides physics steps. They are not * per-space solver tuning; {@link PhysicsSolverSettings} remains part of - * {@link PhysicsSpaceSettings} because solver parameters are applied to individual backend spaces.

+ * per-space solver settings because solver parameters are applied to individual backend spaces.

*/ public class PhysicsWorldSettings { @@ -19,6 +20,10 @@ public class PhysicsWorldSettings { @Nonnull public static final PhysicsStepSchedulingMode DEFAULT_STEP_SCHEDULING_MODE = PhysicsStepSchedulingMode.DROP_PENDING_DT; + /** + * @deprecated Physics event plugin API is deprecated without replacement. + */ + @Deprecated(since = "0.1.0", forRemoval = false) @Nonnull public static final PhysicsEventCollectionMode DEFAULT_EVENT_COLLECTION_MODE = PhysicsEventCollectionMode.DISABLED; @@ -82,11 +87,19 @@ public void setStepSchedulingMode(@Nonnull PhysicsStepSchedulingMode stepSchedul "stepSchedulingMode"); } + /** + * @deprecated Physics event plugin API is deprecated without replacement. + */ + @Deprecated(since = "0.1.0", forRemoval = false) @Nonnull public PhysicsEventCollectionMode getEventCollectionMode() { return eventCollectionMode; } + /** + * @deprecated Physics event plugin API is deprecated without replacement. + */ + @Deprecated(since = "0.1.0", forRemoval = false) public void setEventCollectionMode(@Nonnull PhysicsEventCollectionMode eventCollectionMode) { this.eventCollectionMode = Objects.requireNonNull(eventCollectionMode, "eventCollectionMode"); diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/JointType.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/JointType.java deleted file mode 100644 index 636f710f..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/JointType.java +++ /dev/null @@ -1,12 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; - -/** - * Public joint kinds supported by the copied command recorder. - */ -public enum JointType { - FIXED, - POINT, - HINGE, - SLIDER, - SPRING -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandBatch.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandBatch.java deleted file mode 100644 index fe7123cb..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandBatch.java +++ /dev/null @@ -1,16 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; - -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Public metadata view for a submitted physics simulation command batch. - */ -public record PhysicsCommandBatch(@Nonnull PhysicsCommandMetadata metadata, int commandCount) { - - public PhysicsCommandBatch(@Nonnull PhysicsCommandMetadata metadata, - int commandCount) { - this.metadata = Objects.requireNonNull(metadata, "metadata"); - this.commandCount = Math.max(0, commandCount); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandCompletion.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandCompletion.java deleted file mode 100644 index 253d0924..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandCompletion.java +++ /dev/null @@ -1,146 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; - -import java.util.AbstractList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.RandomAccess; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Completion summary for a submitted physics command batch. - * - *

All-applied and all-rejected summaries may expose lightweight generated result lists instead - * of storing one result object per command. Code that only needs status should prefer - * {@link #allApplied()} and {@link #firstRejected()}.

- */ -public final class PhysicsCommandCompletion { - - @Nonnull - private final List results; - private final boolean allApplied; - @Nullable - private final PhysicsCommandResult firstRejected; - - private PhysicsCommandCompletion(@Nonnull List results, - boolean allApplied, - @Nullable PhysicsCommandResult firstRejected) { - this.results = Objects.requireNonNull(results, "results"); - this.allApplied = allApplied; - this.firstRejected = firstRejected; - } - - @Nonnull - public static PhysicsCommandCompletion of(@Nonnull List results) { - List copied = List.copyOf(results); - PhysicsCommandResult firstRejected = firstRejected(copied); - return new PhysicsCommandCompletion(copied, firstRejected == null, firstRejected); - } - - @Nonnull - public static PhysicsCommandCompletion allApplied(@Nonnull PhysicsCommandMetadata metadata, - int commandCount) { - int size = Math.max(0, commandCount); - List results = size == 0 - ? List.of() - : new AppliedCommandResultList(metadata, size); - return new PhysicsCommandCompletion(results, true, null); - } - - @Nonnull - public static PhysicsCommandCompletion allRejected(@Nonnull PhysicsCommandMetadata metadata, - int commandCount, - @Nonnull String message) { - int size = Math.max(0, commandCount); - if (size == 0) { - return new PhysicsCommandCompletion(List.of(), false, null); - } - String copiedMessage = Objects.requireNonNull(message, "message"); - List results = new RejectedCommandResultList(metadata, - size, - copiedMessage); - return new PhysicsCommandCompletion(results, - false, - PhysicsCommandResult.rejected(metadata, 1L, copiedMessage)); - } - - @Nonnull - public List results() { - return results; - } - - public boolean allApplied() { - return allApplied; - } - - @Nonnull - public Optional firstRejected() { - return Optional.ofNullable(firstRejected); - } - - @Nullable - private static PhysicsCommandResult firstRejected(@Nonnull List results) { - for (PhysicsCommandResult result : results) { - if (result.status() == PhysicsCommandResult.Status.REJECTED) { - return result; - } - } - return null; - } - - private static final class AppliedCommandResultList extends AbstractList - implements RandomAccess { - - @Nonnull - private final PhysicsCommandMetadata metadata; - private final int size; - - private AppliedCommandResultList(@Nonnull PhysicsCommandMetadata metadata, int size) { - this.metadata = Objects.requireNonNull(metadata, "metadata"); - this.size = size; - } - - @Nonnull - @Override - public PhysicsCommandResult get(int index) { - Objects.checkIndex(index, size); - return PhysicsCommandResult.applied(metadata, index + 1L); - } - - @Override - public int size() { - return size; - } - } - - private static final class RejectedCommandResultList extends AbstractList - implements RandomAccess { - - @Nonnull - private final PhysicsCommandMetadata metadata; - private final int size; - @Nonnull - private final String message; - - private RejectedCommandResultList(@Nonnull PhysicsCommandMetadata metadata, - int size, - @Nonnull String message) { - this.metadata = Objects.requireNonNull(metadata, "metadata"); - this.size = size; - this.message = Objects.requireNonNull(message, "message"); - } - - @Nonnull - @Override - public PhysicsCommandResult get(int index) { - Objects.checkIndex(index, size); - return PhysicsCommandResult.rejected(metadata, index + 1L, message); - } - - @Override - public int size() { - return size; - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandContext.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandContext.java deleted file mode 100644 index 35a64708..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandContext.java +++ /dev/null @@ -1,25 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; - -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.PhysicsCommandRecorder; - -import javax.annotation.Nonnull; - -/** - * Plugin-facing authoring context for deferred physics simulation intent. - * - *

This is an authoring surface only. Mutable storage, freezing, and owner-lane - * execution live in the internal simulation package.

- * - *

Commands recorded here are copied into an internal batch before they cross - * the physics-owner boundary. Completion of that batch means the owner lane - * executed the operations; published snapshots may still lag by one or more - * physics frames.

- */ -public interface PhysicsCommandContext extends PhysicsCommandRecorder { - - /** - * Records another recipe into the same pending command batch. - */ - @Nonnull - PhysicsCommandContext compose(@Nonnull PhysicsCommandRecipe recipe); -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandHandle.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandHandle.java deleted file mode 100644 index 6d9cc821..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandHandle.java +++ /dev/null @@ -1,147 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; - -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import javax.annotation.Nonnull; - -/** - * Completion handle for a submitted physics command batch. - * - *

The completion stage reports owner-lane execution of the batch. It does not mean the - * latest published snapshot, ECS attachments, or debug readers have observed the resulting body - * state yet. Use the snapshot-frame inclusion helpers when callers need to correlate a completion - * to a captured physics snapshot frame. Completion callbacks run outside the physics owner lane; - * they may queue follow-up async physics mutations, but synchronous physics owner waits from - * completion callbacks are rejected to avoid callback/owner-lane deadlocks.

- */ -public final class PhysicsCommandHandle { - - @Nonnull - private final PhysicsCommandBatch batch; - @Nonnull - private final CompletableFuture completion; - - private PhysicsCommandHandle(@Nonnull PhysicsCommandBatch batch, - @Nonnull CompletableFuture completion) { - this.batch = Objects.requireNonNull(batch, "batch"); - this.completion = Objects.requireNonNull(completion, "completion"); - } - - @Nonnull - public static PhysicsCommandHandle completed(@Nonnull PhysicsCommandBatch batch, - @Nonnull List results) { - return new PhysicsCommandHandle(batch, - CompletableFuture.completedFuture(PhysicsCommandCompletion.of(results))); - } - - @Nonnull - public static PhysicsCommandHandle failed(@Nonnull PhysicsCommandBatch batch, - @Nonnull Throwable failure) { - CompletableFuture completion = new CompletableFuture<>(); - completion.completeExceptionally(Objects.requireNonNull(failure, "failure")); - return new PhysicsCommandHandle(batch, completion); - } - - @Nonnull - public static PhysicsCommandHandle fromCompletion(@Nonnull PhysicsCommandBatch batch, - @Nonnull CompletionStage> completion) { - return new PhysicsCommandHandle(batch, - Objects.requireNonNull(completion, "completion") - .thenApply(PhysicsCommandCompletion::of) - .toCompletableFuture()); - } - - @Nonnull - public static PhysicsCommandHandle fromCompletionSummary(@Nonnull PhysicsCommandBatch batch, - @Nonnull CompletionStage completion) { - return new PhysicsCommandHandle(batch, - Objects.requireNonNull(completion, "completion").toCompletableFuture()); - } - - @Nonnull - public PhysicsCommandBatch batch() { - return batch; - } - - /** - * Returns whether the latest captured snapshot in {@code frame} is known to include this - * submitted command batch. - */ - public boolean isIncludedInLatestCapturedSnapshot(@Nonnull PhysicsEventFrame frame) { - return Objects.requireNonNull(frame, "frame") - .latestCapturedSnapshotIncludesCommandBatch(batch.metadata().commandBatchSequence()); - } - - /** - * Returns the submitted-server-tick distance from this command batch to the latest captured - * snapshot in {@code frame}, or {@code 0} when that snapshot is not known to include this batch. - */ - public long capturedSnapshotServerTickLatency(@Nonnull PhysicsEventFrame frame) { - PhysicsEventFrame eventFrame = Objects.requireNonNull(frame, "frame"); - if (!eventFrame.latestCapturedSnapshotIncludesCommandBatch(batch.metadata().commandBatchSequence())) { - return 0L; - } - return eventFrame.latestCapturedSnapshotServerTickLatencyFromSubmittedTick( - batch.metadata().submittedServerTick()); - } - - /** - * Returns whether {@code frame} is known to include this submitted command batch. - */ - public boolean isIncludedInSnapshotFrame(@Nonnull PublishedPhysicsSnapshotFrame frame) { - return Objects.requireNonNull(frame, "frame") - .includesCommandBatch(batch.metadata().commandBatchSequence()); - } - - /** - * Returns the submitted-server-tick distance from this command batch to {@code frame}, or - * {@code 0} when the snapshot is not known to include this batch. - */ - public long capturedSnapshotServerTickLatency(@Nonnull PublishedPhysicsSnapshotFrame frame) { - PublishedPhysicsSnapshotFrame snapshotFrame = Objects.requireNonNull(frame, "frame"); - if (!snapshotFrame.includesCommandBatch(batch.metadata().commandBatchSequence())) { - return 0L; - } - return snapshotFrame.serverTickLatencyFromSubmittedTick( - batch.metadata().submittedServerTick()); - } - - @Nonnull - public CompletionStage> completion() { - return completion.thenApply(PhysicsCommandCompletion::results); - } - - /** - * Returns the owner-lane execution summary for this batch. - * - *

This stage completes before snapshot capture, reader-side snapshot application, and ECS - * consumption. Use {@link #isIncludedInSnapshotFrame(PublishedPhysicsSnapshotFrame)} or - * {@link #isIncludedInLatestCapturedSnapshot(PhysicsEventFrame)} when snapshot-frame inclusion - * matters.

- */ - @Nonnull - public CompletionStage completionSummary() { - return completion.minimalCompletionStage(); - } - - /** - * Returns whether every recorded operation applied on the physics owner lane. - */ - @Nonnull - public CompletionStage allApplied() { - return completion.thenApply(PhysicsCommandCompletion::allApplied); - } - - /** - * Returns the first owner-lane rejection, if any. - */ - @Nonnull - public CompletionStage> firstRejected() { - return completion.thenApply(PhysicsCommandCompletion::firstRejected); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandMetadata.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandMetadata.java deleted file mode 100644 index 016693f7..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandMetadata.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; - -/** - * Correlation data assigned when a recorded physics command batch is frozen. - */ -public record PhysicsCommandMetadata(long submittedServerTick, - long commandBatchSequence) { - - public PhysicsCommandMetadata { - submittedServerTick = Math.max(0L, submittedServerTick); - commandBatchSequence = Math.max(0L, commandBatchSequence); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandRecipe.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandRecipe.java deleted file mode 100644 index 36468ef3..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandRecipe.java +++ /dev/null @@ -1,17 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; - -import javax.annotation.Nonnull; - -/** - * Plugin-defined command authoring recipe. - * - *

Recipes run immediately on the caller thread and record copied value - * commands into the supplied command context. They are the preferred way for plugins - * to expose higher-level reusable physics operations without requiring Impulse - * to execute unknown command classes.

- */ -@FunctionalInterface -public interface PhysicsCommandRecipe { - - void record(@Nonnull PhysicsCommandContext commands); -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandResult.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandResult.java deleted file mode 100644 index 18440cae..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandResult.java +++ /dev/null @@ -1,75 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; - -import java.util.Objects; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Copied result for one physics command. - * - * @param commandBatchSequence owner FIFO batch sequence assigned when the command batch was submitted - * @param submittedServerTick server tick supplied by the caller when the command context was recorded - * @param includedSnapshotFrameEpoch published snapshot frame that is known to include this command, - * or {@code 0} when completion has not been correlated to a - * snapshot frame - */ -public record PhysicsCommandResult(@Nonnull Status status, - long commandSequence, - long commandBatchSequence, - long submittedServerTick, - long includedSnapshotFrameEpoch, - @Nullable String message) { - - public PhysicsCommandResult { - Objects.requireNonNull(status, "status"); - commandSequence = Math.max(0L, commandSequence); - commandBatchSequence = Math.max(0L, commandBatchSequence); - submittedServerTick = Math.max(0L, submittedServerTick); - includedSnapshotFrameEpoch = Math.max(0L, includedSnapshotFrameEpoch); - } - - @Nonnull - public static PhysicsCommandResult applied(long commandSequence) { - return new PhysicsCommandResult(Status.APPLIED, commandSequence, 0L, 0L, 0L, null); - } - - @Nonnull - public static PhysicsCommandResult applied(@Nonnull PhysicsCommandMetadata metadata, - long commandSequence) { - Objects.requireNonNull(metadata, "metadata"); - return new PhysicsCommandResult(Status.APPLIED, - commandSequence, - metadata.commandBatchSequence(), - metadata.submittedServerTick(), - 0L, - null); - } - - @Nonnull - public static PhysicsCommandResult rejected(long commandSequence, @Nonnull String message) { - return new PhysicsCommandResult(Status.REJECTED, - commandSequence, - 0L, - 0L, - 0L, - Objects.requireNonNull(message, "message")); - } - - @Nonnull - public static PhysicsCommandResult rejected(@Nonnull PhysicsCommandMetadata metadata, - long commandSequence, - @Nonnull String message) { - Objects.requireNonNull(metadata, "metadata"); - return new PhysicsCommandResult(Status.REJECTED, - commandSequence, - metadata.commandBatchSequence(), - metadata.submittedServerTick(), - 0L, - Objects.requireNonNull(message, "message")); - } - - public enum Status { - APPLIED, - REJECTED - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsFalloff.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsFalloff.java deleted file mode 100644 index 99b4cf83..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsFalloff.java +++ /dev/null @@ -1,39 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; - -import javax.annotation.Nonnull; - -/** - * Scalar attenuation for area physics command recipes. - */ -@FunctionalInterface -public interface PhysicsFalloff { - - PhysicsFalloff CONSTANT = (distance, radius) -> distance <= radius ? 1.0f : 0.0f; - PhysicsFalloff LINEAR = (distance, radius) -> radius > 0.0f - ? Math.max(0.0f, 1.0f - distance / radius) - : 0.0f; - PhysicsFalloff SMOOTH = (distance, radius) -> { - if (radius <= 0.0f) { - return 0.0f; - } - float t = Math.max(0.0f, Math.min(1.0f, distance / radius)); - return 1.0f - (t * t * (3.0f - 2.0f * t)); - }; - - float scale(float distance, float radius); - - @Nonnull - static PhysicsFalloff constant() { - return CONSTANT; - } - - @Nonnull - static PhysicsFalloff linear() { - return LINEAR; - } - - @Nonnull - static PhysicsFalloff smooth() { - return SMOOTH; - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsRecipes.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsRecipes.java deleted file mode 100644 index 55cb6b18..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsRecipes.java +++ /dev/null @@ -1,193 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; - -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.snapshot.PhysicsBodySnapshotEntry; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -/** - * Small reusable physics command recipes for common gameplay impulses and forces. - */ -public final class PhysicsRecipes { - - private static final float EPSILON = 0.000001f; - private static final PhysicsCommandRecipe EMPTY = commands -> { - }; - - private PhysicsRecipes() { - } - - @Nonnull - public static PhysicsCommandRecipe applyImpulse(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f impulse) { - Objects.requireNonNull(bodyKey, "bodyKey"); - Objects.requireNonNull(impulse, "impulse"); - float x = finite(impulse.x, "impulse.x"); - float y = finite(impulse.y, "impulse.y"); - float z = finite(impulse.z, "impulse.z"); - return commands -> commands.applyBodyImpulse(bodyKey, x, y, z); - } - - @Nonnull - public static PhysicsCommandRecipe applyImpulse(@Nonnull Iterable bodyKeys, - @Nonnull Vector3f impulse) { - List keys = copyBodyKeys(bodyKeys); - Objects.requireNonNull(impulse, "impulse"); - float x = finite(impulse.x, "impulse.x"); - float y = finite(impulse.y, "impulse.y"); - float z = finite(impulse.z, "impulse.z"); - if (keys.isEmpty()) { - return EMPTY; - } - return commands -> { - for (RigidBodyKey bodyKey : keys) { - commands.applyBodyImpulse(bodyKey, x, y, z); - } - }; - } - - @Nonnull - public static PhysicsCommandRecipe applyForce(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f force) { - Objects.requireNonNull(bodyKey, "bodyKey"); - Objects.requireNonNull(force, "force"); - float x = finite(force.x, "force.x"); - float y = finite(force.y, "force.y"); - float z = finite(force.z, "force.z"); - return commands -> commands.applyBodyForce(bodyKey, x, y, z); - } - - @Nonnull - public static PhysicsCommandRecipe applyForce(@Nonnull Iterable bodyKeys, - @Nonnull Vector3f force) { - List keys = copyBodyKeys(bodyKeys); - Objects.requireNonNull(force, "force"); - float x = finite(force.x, "force.x"); - float y = finite(force.y, "force.y"); - float z = finite(force.z, "force.z"); - if (keys.isEmpty()) { - return EMPTY; - } - return commands -> { - for (RigidBodyKey bodyKey : keys) { - commands.applyBodyForce(bodyKey, x, y, z); - } - }; - } - - @Nonnull - public static PhysicsCommandRecipe radialImpulse( - @Nonnull Iterable bodies, - @Nonnull Vector3f origin, - float strength, - float radius, - @Nonnull PhysicsFalloff falloff) { - List commands = radialCommands(bodies, origin, strength, radius, falloff); - if (commands.isEmpty()) { - return EMPTY; - } - return context -> { - for (VectorCommand command : commands) { - context.applyBodyImpulse(command.bodyKey(), command.x(), command.y(), command.z()); - } - }; - } - - @Nonnull - public static PhysicsCommandRecipe radialForce( - @Nonnull Iterable bodies, - @Nonnull Vector3f origin, - float strength, - float radius, - @Nonnull PhysicsFalloff falloff) { - List commands = radialCommands(bodies, origin, strength, radius, falloff); - if (commands.isEmpty()) { - return EMPTY; - } - return context -> { - for (VectorCommand command : commands) { - context.applyBodyForce(command.bodyKey(), command.x(), command.y(), command.z()); - } - }; - } - - @Nonnull - private static List copyBodyKeys(@Nonnull Iterable bodyKeys) { - Objects.requireNonNull(bodyKeys, "bodyKeys"); - List keys = new ArrayList<>(); - for (RigidBodyKey bodyKey : bodyKeys) { - keys.add(Objects.requireNonNull(bodyKey, "bodyKey")); - } - return List.copyOf(keys); - } - - @Nonnull - private static List radialCommands( - @Nonnull Iterable bodies, - @Nonnull Vector3f origin, - float strength, - float radius, - @Nonnull PhysicsFalloff falloff) { - Objects.requireNonNull(bodies, "bodies"); - Objects.requireNonNull(origin, "origin"); - Objects.requireNonNull(falloff, "falloff"); - float originX = finite(origin.x, "origin.x"); - float originY = finite(origin.y, "origin.y"); - float originZ = finite(origin.z, "origin.z"); - float finiteStrength = finite(strength, "strength"); - float finiteRadius = finite(radius, "radius"); - if (finiteRadius <= 0.0f || finiteStrength == 0.0f) { - return List.of(); - } - - List commands = new ArrayList<>(); - for (PhysicsBodySnapshotEntry entry : bodies) { - Objects.requireNonNull(entry, "entry"); - if (!entry.snapshot().isDynamic() || entry.snapshot().sensor()) { - continue; - } - float dx = entry.snapshot().positionX() - originX; - float dy = entry.snapshot().positionY() - originY; - float dz = entry.snapshot().positionZ() - originZ; - float distanceSquared = dx * dx + dy * dy + dz * dz; - if (distanceSquared <= EPSILON || distanceSquared > finiteRadius * finiteRadius) { - continue; - } - float distance = (float) Math.sqrt(distanceSquared); - float scale = safeScale(falloff.scale(distance, finiteRadius)); - if (scale <= 0.0f) { - continue; - } - float magnitude = finiteStrength * scale / distance; - commands.add(new VectorCommand(entry.bodyKey(), - dx * magnitude, - dy * magnitude, - dz * magnitude)); - } - return List.copyOf(commands); - } - - private static float safeScale(float scale) { - if (!Float.isFinite(scale)) { - return 0.0f; - } - return Math.max(0.0f, Math.min(1.0f, scale)); - } - - private static float finite(float value, @Nonnull String name) { - if (!Float.isFinite(value)) { - throw new IllegalArgumentException(name + " must be finite"); - } - return value; - } - - private record VectorCommand(@Nonnull RigidBodyKey bodyKey, float x, float y, float z) { - - private VectorCommand { - Objects.requireNonNull(bodyKey, "bodyKey"); - } - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/RigidBodyPose.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/RigidBodyPose.java deleted file mode 100644 index 685d92e2..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/RigidBodyPose.java +++ /dev/null @@ -1,36 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; - -import java.util.Objects; -import javax.annotation.Nonnull; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -/** - * Copied rigid body transform. - */ -public record RigidBodyPose(@Nonnull Vector3f position, - @Nonnull Quaternionf rotation) { - - public RigidBodyPose { - position = new Vector3f(Objects.requireNonNull(position, "position")); - rotation = new Quaternionf(Objects.requireNonNull(rotation, "rotation")); - } - - @Nonnull - public static RigidBodyPose of(@Nonnull Vector3f position, - @Nonnull Quaternionf rotation) { - return new RigidBodyPose(position, rotation); - } - - @Nonnull - @Override - public Vector3f position() { - return new Vector3f(position); - } - - @Nonnull - @Override - public Quaternionf rotation() { - return new Quaternionf(rotation); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/SpaceSummary.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/SpaceSummary.java deleted file mode 100644 index 2f83f17f..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/SpaceSummary.java +++ /dev/null @@ -1,22 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.SpaceId; -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Copied owner-lane space diagnostics. - */ -public record SpaceSummary(@Nonnull SpaceId spaceId, - @Nonnull BackendId backendId, - int bodyCount, - int jointCount) { - - public SpaceSummary { - Objects.requireNonNull(spaceId, "spaceId"); - Objects.requireNonNull(backendId, "backendId"); - bodyCount = Math.max(0, bodyCount); - jointCount = Math.max(0, jointCount); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/CcdSupportQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/CcdSupportQuery.java deleted file mode 100644 index aec945f6..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/CcdSupportQuery.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.query; - -/** - * Owner-lane query for whether every registered space supports continuous collision detection. - */ -public record CcdSupportQuery() implements PhysicsQuery { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/PhysicsQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/PhysicsQuery.java deleted file mode 100644 index 0940d36b..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/PhysicsQuery.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.query; - -/** - * Data-only owner-lane read that returns copied physics data. - * - * @param immutable query result type - */ -public sealed interface PhysicsQuery permits RaycastClosestQuery, - RaycastClosestBatchQuery, - RaycastAllQuery, - SpaceBodyCountQuery, - SpaceSummaryQuery, - CcdSupportQuery, - UnsupportedCcdSpacesQuery, - SolverCapabilityQuery, - RigidBodyStateQuery, - RuntimeJointCountQuery { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/PhysicsQueryHandle.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/PhysicsQueryHandle.java deleted file mode 100644 index 8257d817..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/PhysicsQueryHandle.java +++ /dev/null @@ -1,58 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.query; - -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import javax.annotation.Nonnull; - -/** - * Completion handle for a typed physics owner query. - * - *

Queries run on the physics owner and return copied values. They are intended for - * read-only inspection that needs live backend state without exposing backend handles to plugin - * code.

- */ -public final class PhysicsQueryHandle { - - @Nonnull - private final PhysicsQuery query; - @Nonnull - private final CompletableFuture completion; - - private PhysicsQueryHandle(@Nonnull PhysicsQuery query, - @Nonnull CompletableFuture completion) { - this.query = Objects.requireNonNull(query, "query"); - this.completion = Objects.requireNonNull(completion, "completion"); - } - - @Nonnull - public static PhysicsQueryHandle completed(@Nonnull PhysicsQuery query, - @Nonnull R value) { - return new PhysicsQueryHandle<>(query, CompletableFuture.completedFuture(value)); - } - - @Nonnull - public static PhysicsQueryHandle failed(@Nonnull PhysicsQuery query, - @Nonnull Throwable failure) { - CompletableFuture completion = new CompletableFuture<>(); - completion.completeExceptionally(Objects.requireNonNull(failure, "failure")); - return new PhysicsQueryHandle<>(query, completion); - } - - @Nonnull - public static PhysicsQueryHandle fromCompletion(@Nonnull PhysicsQuery query, - @Nonnull CompletionStage completion) { - return new PhysicsQueryHandle<>(query, - Objects.requireNonNull(completion, "completion").toCompletableFuture()); - } - - @Nonnull - public PhysicsQuery query() { - return query; - } - - @Nonnull - public CompletionStage completion() { - return completion.minimalCompletionStage(); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/RaycastAllQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/RaycastAllQuery.java deleted file mode 100644 index c104cb9a..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/RaycastAllQuery.java +++ /dev/null @@ -1,38 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.query; - -import dev.hytalemodding.impulse.api.SpaceId; -import java.util.List; -import java.util.Objects; -import javax.annotation.Nonnull; - -import dev.hytalemodding.impulse.core.plugin.simulation.view.RaycastHitView; -import org.joml.Vector3f; - -/** - * Owner-lane query for every ray hit in one physics space. - * - *

The endpoints are defensively copied so callers can reuse mutable vector instances after - * submission.

- */ -public record RaycastAllQuery(@Nonnull SpaceId spaceId, - @Nonnull Vector3f from, - @Nonnull Vector3f to) implements PhysicsQuery> { - - public RaycastAllQuery { - Objects.requireNonNull(spaceId, "spaceId"); - from = new Vector3f(Objects.requireNonNull(from, "from")); - to = new Vector3f(Objects.requireNonNull(to, "to")); - } - - @Nonnull - @Override - public Vector3f from() { - return new Vector3f(from); - } - - @Nonnull - @Override - public Vector3f to() { - return new Vector3f(to); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/RaycastClosestBatchQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/RaycastClosestBatchQuery.java deleted file mode 100644 index 4e2a0135..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/RaycastClosestBatchQuery.java +++ /dev/null @@ -1,39 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.query; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.simulation.RaycastClosestBatchResult; -import dev.hytalemodding.impulse.core.plugin.simulation.RaycastSegment; - -import java.util.List; -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Owner-lane query for closest hits across many ray segments in one physics space. - * - *

The ray list is copied on construction. Result indexes match the input ray order.

- */ -public record RaycastClosestBatchQuery(@Nonnull SpaceId spaceId, - @Nonnull List rays) - implements PhysicsQuery { - - public RaycastClosestBatchQuery(@Nonnull SpaceId spaceId, - @Nonnull List rays) { - this.spaceId = Objects.requireNonNull(spaceId, "spaceId"); - this.rays = List.copyOf(Objects.requireNonNull(rays, "rays")); - } - - public int rayCount() { - return rays.size(); - } - - @Nonnull - public RaycastSegment ray(int index) { - return rays.get(index); - } - - @Nonnull - public List rays() { - return List.copyOf(rays); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/RaycastClosestQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/RaycastClosestQuery.java deleted file mode 100644 index f5415c4b..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/RaycastClosestQuery.java +++ /dev/null @@ -1,38 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.query; - -import dev.hytalemodding.impulse.api.SpaceId; -import java.util.Objects; -import java.util.Optional; -import javax.annotation.Nonnull; - -import dev.hytalemodding.impulse.core.plugin.simulation.view.RaycastHitView; -import org.joml.Vector3f; - -/** - * Owner-lane query for the nearest ray hit in one physics space. - * - *

The endpoints are defensively copied so callers can reuse mutable vector instances after - * submission.

- */ -public record RaycastClosestQuery(@Nonnull SpaceId spaceId, - @Nonnull Vector3f from, - @Nonnull Vector3f to) implements PhysicsQuery> { - - public RaycastClosestQuery { - Objects.requireNonNull(spaceId, "spaceId"); - from = new Vector3f(Objects.requireNonNull(from, "from")); - to = new Vector3f(Objects.requireNonNull(to, "to")); - } - - @Nonnull - @Override - public Vector3f from() { - return new Vector3f(from); - } - - @Nonnull - @Override - public Vector3f to() { - return new Vector3f(to); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/RigidBodyStateQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/RigidBodyStateQuery.java deleted file mode 100644 index 401b8975..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/RigidBodyStateQuery.java +++ /dev/null @@ -1,19 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.query; - -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.view.RigidBodyStateView; - -import java.util.Objects; -import java.util.Optional; -import javax.annotation.Nonnull; - -/** - * Owner-lane query for one rigid body's current live state copied by stable body key. - */ -public record RigidBodyStateQuery(@Nonnull RigidBodyKey bodyKey) - implements PhysicsQuery> { - - public RigidBodyStateQuery { - Objects.requireNonNull(bodyKey, "bodyKey"); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/RuntimeJointCountQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/RuntimeJointCountQuery.java deleted file mode 100644 index b14a48d6..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/RuntimeJointCountQuery.java +++ /dev/null @@ -1,7 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.query; - -/** - * Owner-lane query for the current live joint count across all physics spaces. - */ -public record RuntimeJointCountQuery() implements PhysicsQuery { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/SolverCapabilityQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/SolverCapabilityQuery.java deleted file mode 100644 index 11b9a50a..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/SolverCapabilityQuery.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.query; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.simulation.SolverCapabilitySummary; - -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Owner-lane query for backend solver-tuning support in one physics space. - */ -public record SolverCapabilityQuery(@Nonnull SpaceId spaceId) - implements PhysicsQuery { - - public SolverCapabilityQuery { - Objects.requireNonNull(spaceId, "spaceId"); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/SpaceBodyCountQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/SpaceBodyCountQuery.java deleted file mode 100644 index 04e78890..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/SpaceBodyCountQuery.java +++ /dev/null @@ -1,16 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.query; - -import dev.hytalemodding.impulse.api.SpaceId; - -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Owner-lane query for the current live rigid body count in one physics space. - */ -public record SpaceBodyCountQuery(@Nonnull SpaceId spaceId) implements PhysicsQuery { - - public SpaceBodyCountQuery { - Objects.requireNonNull(spaceId, "spaceId"); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/SpaceSummaryQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/SpaceSummaryQuery.java deleted file mode 100644 index 4f28217f..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/SpaceSummaryQuery.java +++ /dev/null @@ -1,15 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.query; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.simulation.SpaceSummary; - -import java.util.List; -import javax.annotation.Nullable; - -/** - * Owner-lane query for copied space diagnostics. - * - *

A {@code null} space id requests summaries for every registered physics space.

- */ -public record SpaceSummaryQuery(@Nullable SpaceId spaceId) implements PhysicsQuery> { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/UnsupportedCcdSpacesQuery.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/UnsupportedCcdSpacesQuery.java deleted file mode 100644 index 98239de2..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/query/UnsupportedCcdSpacesQuery.java +++ /dev/null @@ -1,11 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.query; - -import dev.hytalemodding.impulse.core.plugin.simulation.SpaceSummary; - -import java.util.List; - -/** - * Owner-lane query for spaces whose backend does not support continuous collision detection. - */ -public record UnsupportedCcdSpacesQuery() implements PhysicsQuery> { -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/JointCommandRecorder.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/JointCommandRecorder.java deleted file mode 100644 index 3ead5c17..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/JointCommandRecorder.java +++ /dev/null @@ -1,101 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.recorder; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -/** - * Fluent recorder for one joint creation command. - */ -public interface JointCommandRecorder { - - @Nonnull - JointCommandRecorder space(@Nonnull SpaceId spaceId); - - @Nonnull - JointCommandRecorder bodies(@Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB); - - @Nonnull - JointCommandRecorder fixed(@Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB); - - @Nonnull - JointCommandRecorder fixed(float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ); - - @Nonnull - JointCommandRecorder point(@Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB); - - @Nonnull - JointCommandRecorder point(float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ); - - @Nonnull - JointCommandRecorder hinge(@Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis); - - @Nonnull - JointCommandRecorder hinge(float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ); - - @Nonnull - JointCommandRecorder slider(@Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis); - - @Nonnull - JointCommandRecorder slider(float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ); - - @Nonnull - JointCommandRecorder spring(@Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - float restLength, - float stiffness, - float damping); - - @Nonnull - JointCommandRecorder spring(float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float restLength, - float stiffness, - float damping); - - @Nonnull - JointCommandRecorder limits(float lowerLimit, - float upperLimit); - - @Nonnull - JointCommandRecorder motor(float targetVelocity, - float maxForce); -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/PhysicsCommandRecorder.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/PhysicsCommandRecorder.java deleted file mode 100644 index 96230f08..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/PhysicsCommandRecorder.java +++ /dev/null @@ -1,333 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.recorder; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import java.util.Objects; -import java.util.function.Consumer; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -/** - * Fluent recorder for copied physics simulation intent. - * - *

The word "command" here means a compact, value-copied request submitted to the physics - * owner. It is not a Hytale server command and it should not be modeled as one Java object per - * mechanical instruction for high-volume cases. Use the bulk/template spawn methods when many - * bodies share the same shape and settings.

- */ -public interface PhysicsCommandRecorder { - - @Nonnull - RigidBodyCommandRecorder body(@Nonnull RigidBodyKey bodyKey); - - @Nonnull - PhysicsCommandRecorder body(@Nonnull RigidBodyKey bodyKey, - @Nonnull Consumer recipe); - - @Nonnull - PhysicsCommandRecorder setSpaceGravity(@Nonnull SpaceId spaceId, - float x, - float y, - float z); - - @Nonnull - default PhysicsCommandRecorder setBodyTransform(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f position, - @Nonnull Quaternionf rotation) { - return setBodyTransform(bodyKey, position, rotation, false); - } - - @Nonnull - default PhysicsCommandRecorder setBodyTransform(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f position, - @Nonnull Quaternionf rotation, - boolean activate) { - Objects.requireNonNull(position, "position"); - Objects.requireNonNull(rotation, "rotation"); - return setBodyTransform(bodyKey, - position.x, - position.y, - position.z, - rotation.x, - rotation.y, - rotation.z, - rotation.w, - activate); - } - - @Nonnull - default PhysicsCommandRecorder setBodyTransform(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ, - float rotationX, - float rotationY, - float rotationZ, - float rotationW, - boolean activate) { - body(bodyKey).setTransform(positionX, - positionY, - positionZ, - rotationX, - rotationY, - rotationZ, - rotationW, - activate); - return this; - } - - @Nonnull - default PhysicsCommandRecorder setBodyPosition(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f position) { - return setBodyPosition(bodyKey, position, false); - } - - @Nonnull - default PhysicsCommandRecorder setBodyPosition(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f position, - boolean activate) { - Objects.requireNonNull(position, "position"); - return setBodyPosition(bodyKey, position.x, position.y, position.z, activate); - } - - @Nonnull - default PhysicsCommandRecorder setBodyPosition(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ, - boolean activate) { - body(bodyKey).setPosition(positionX, positionY, positionZ, activate); - return this; - } - - @Nonnull - default PhysicsCommandRecorder setBodyVelocity(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f linearVelocity, - @Nonnull Vector3f angularVelocity) { - return setBodyVelocity(bodyKey, linearVelocity, angularVelocity, false); - } - - @Nonnull - default PhysicsCommandRecorder setBodyVelocity(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f linearVelocity, - @Nonnull Vector3f angularVelocity, - boolean activate) { - Objects.requireNonNull(linearVelocity, "linearVelocity"); - Objects.requireNonNull(angularVelocity, "angularVelocity"); - return setBodyVelocity(bodyKey, - linearVelocity.x, - linearVelocity.y, - linearVelocity.z, - angularVelocity.x, - angularVelocity.y, - angularVelocity.z, - activate); - } - - @Nonnull - default PhysicsCommandRecorder setBodyVelocity(@Nonnull RigidBodyKey bodyKey, - float linearX, - float linearY, - float linearZ, - float angularX, - float angularY, - float angularZ, - boolean activate) { - body(bodyKey).setVelocity(linearX, - linearY, - linearZ, - angularX, - angularY, - angularZ, - activate); - return this; - } - - @Nonnull - default PhysicsCommandRecorder setBodyType(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodyType bodyType) { - return setBodyType(bodyKey, bodyType, false); - } - - @Nonnull - default PhysicsCommandRecorder setBodyType(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodyType bodyType, - boolean activate) { - body(bodyKey).setType(bodyType, activate); - return this; - } - - @Nonnull - default PhysicsCommandRecorder activateBody(@Nonnull RigidBodyKey bodyKey) { - body(bodyKey).activate(); - return this; - } - - @Nonnull - default PhysicsCommandRecorder applyBodyImpulse(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f impulse) { - Objects.requireNonNull(impulse, "impulse"); - return applyBodyImpulse(bodyKey, impulse.x, impulse.y, impulse.z); - } - - @Nonnull - default PhysicsCommandRecorder applyBodyImpulse(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z) { - body(bodyKey).applyImpulse(x, y, z); - return this; - } - - @Nonnull - default PhysicsCommandRecorder applyBodyImpulse(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f impulse, - @Nonnull Vector3f offset) { - Objects.requireNonNull(impulse, "impulse"); - Objects.requireNonNull(offset, "offset"); - return applyBodyImpulse(bodyKey, impulse.x, impulse.y, impulse.z, offset.x, offset.y, offset.z); - } - - @Nonnull - default PhysicsCommandRecorder applyBodyImpulse(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ) { - body(bodyKey).applyImpulse(x, y, z, offsetX, offsetY, offsetZ); - return this; - } - - @Nonnull - default PhysicsCommandRecorder applyBodyTorqueImpulse(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f impulse) { - Objects.requireNonNull(impulse, "impulse"); - return applyBodyTorqueImpulse(bodyKey, impulse.x, impulse.y, impulse.z); - } - - @Nonnull - default PhysicsCommandRecorder applyBodyTorqueImpulse(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z) { - body(bodyKey).applyTorqueImpulse(x, y, z); - return this; - } - - @Nonnull - default PhysicsCommandRecorder applyBodyForce(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f force) { - Objects.requireNonNull(force, "force"); - return applyBodyForce(bodyKey, force.x, force.y, force.z); - } - - @Nonnull - default PhysicsCommandRecorder applyBodyForce(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z) { - body(bodyKey).applyForce(x, y, z); - return this; - } - - @Nonnull - default PhysicsCommandRecorder applyBodyForce(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f force, - @Nonnull Vector3f offset) { - Objects.requireNonNull(force, "force"); - Objects.requireNonNull(offset, "offset"); - return applyBodyForce(bodyKey, force.x, force.y, force.z, offset.x, offset.y, offset.z); - } - - @Nonnull - default PhysicsCommandRecorder applyBodyForce(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ) { - body(bodyKey).applyForce(x, y, z, offsetX, offsetY, offsetZ); - return this; - } - - @Nonnull - default PhysicsCommandRecorder applyBodyTorque(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f force) { - Objects.requireNonNull(force, "force"); - return applyBodyTorque(bodyKey, force.x, force.y, force.z); - } - - @Nonnull - default PhysicsCommandRecorder applyBodyTorque(@Nonnull RigidBodyKey bodyKey, - float x, - float y, - float z) { - body(bodyKey).applyTorque(x, y, z); - return this; - } - - @Nonnull - default PhysicsCommandRecorder destroyBody(@Nonnull RigidBodyKey bodyKey) { - body(bodyKey).destroy(); - return this; - } - - @Nonnull - RigidBodySpawnRecorder spawnBody(@Nonnull RigidBodyKey bodyKey); - - @Nonnull - PhysicsCommandRecorder spawnBody(@Nonnull RigidBodyKey bodyKey, - @Nonnull Consumer recipe); - - @Nonnull - PhysicsCommandRecorder spawnBodies(@Nonnull Consumer recipe); - - /** - * Records a bulk spawn batch with a capacity hint for the number of bodies the recipe will add. - */ - @Nonnull - PhysicsCommandRecorder spawnBodies(int expectedBodies, - @Nonnull Consumer recipe); - - /** - * Records a compact template spawn batch where each body differs only by key and position. - */ - @Nonnull - PhysicsCommandRecorder spawnBodies(int expectedBodies, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode, - @Nonnull Consumer recipe); - - @Nonnull - JointCommandRecorder joint(@Nonnull JointKey jointKey); - - @Nonnull - PhysicsCommandRecorder joint(@Nonnull JointKey jointKey, - @Nonnull Consumer recipe); - - @Nonnull - PhysicsCommandRecorder destroyJoint(@Nonnull JointKey jointKey); - - @Nonnull - PhysicsCommandRecorder destroyJointBetween(@Nullable JointKey preferredJointKey, - @Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB); -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/RigidBodyCommandRecorder.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/RigidBodyCommandRecorder.java deleted file mode 100644 index 142d768b..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/RigidBodyCommandRecorder.java +++ /dev/null @@ -1,131 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.recorder; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import javax.annotation.Nonnull; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -/** - * Fluent recorder for operations targeting one rigid body key. - */ -public interface RigidBodyCommandRecorder { - - @Nonnull - RigidBodyCommandRecorder setTransform(@Nonnull Vector3f position, - @Nonnull Quaternionf rotation); - - @Nonnull - RigidBodyCommandRecorder setTransform(@Nonnull Vector3f position, - @Nonnull Quaternionf rotation, - boolean activate); - - @Nonnull - RigidBodyCommandRecorder setTransform(float positionX, - float positionY, - float positionZ, - float rotationX, - float rotationY, - float rotationZ, - float rotationW, - boolean activate); - - @Nonnull - RigidBodyCommandRecorder setPosition(@Nonnull Vector3f position); - - @Nonnull - RigidBodyCommandRecorder setPosition(@Nonnull Vector3f position, - boolean activate); - - @Nonnull - RigidBodyCommandRecorder setPosition(float positionX, - float positionY, - float positionZ, - boolean activate); - - @Nonnull - RigidBodyCommandRecorder setVelocity(@Nonnull Vector3f linearVelocity, - @Nonnull Vector3f angularVelocity); - - @Nonnull - RigidBodyCommandRecorder setVelocity(@Nonnull Vector3f linearVelocity, - @Nonnull Vector3f angularVelocity, - boolean activate); - - @Nonnull - RigidBodyCommandRecorder setVelocity(float linearX, - float linearY, - float linearZ, - float angularX, - float angularY, - float angularZ, - boolean activate); - - @Nonnull - RigidBodyCommandRecorder setType(@Nonnull PhysicsBodyType bodyType); - - @Nonnull - RigidBodyCommandRecorder setType(@Nonnull PhysicsBodyType bodyType, - boolean activate); - - @Nonnull - RigidBodyCommandRecorder activate(); - - @Nonnull - RigidBodyCommandRecorder applyImpulse(@Nonnull Vector3f impulse); - - @Nonnull - RigidBodyCommandRecorder applyImpulse(float x, - float y, - float z); - - @Nonnull - RigidBodyCommandRecorder applyImpulse(@Nonnull Vector3f impulse, - @Nonnull Vector3f offset); - - @Nonnull - RigidBodyCommandRecorder applyImpulse(float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ); - - @Nonnull - RigidBodyCommandRecorder applyTorqueImpulse(@Nonnull Vector3f impulse); - - @Nonnull - RigidBodyCommandRecorder applyTorqueImpulse(float x, - float y, - float z); - - @Nonnull - RigidBodyCommandRecorder applyForce(@Nonnull Vector3f force); - - @Nonnull - RigidBodyCommandRecorder applyForce(float x, - float y, - float z); - - @Nonnull - RigidBodyCommandRecorder applyForce(@Nonnull Vector3f force, - @Nonnull Vector3f offset); - - @Nonnull - RigidBodyCommandRecorder applyForce(float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ); - - @Nonnull - RigidBodyCommandRecorder applyTorque(@Nonnull Vector3f force); - - @Nonnull - RigidBodyCommandRecorder applyTorque(float x, - float y, - float z); - - @Nonnull - RigidBodyCommandRecorder destroy(); -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/RigidBodySpawnBatchRecorder.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/RigidBodySpawnBatchRecorder.java deleted file mode 100644 index a91ac3c5..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/RigidBodySpawnBatchRecorder.java +++ /dev/null @@ -1,49 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.recorder; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; - -import java.util.function.Consumer; -import javax.annotation.Nonnull; - -/** - * Fluent recorder for a bulk rigid body spawn command. - */ -public interface RigidBodySpawnBatchRecorder { - - @Nonnull - RigidBodySpawnBatchRecorder body(@Nonnull RigidBodyKey bodyKey, - @Nonnull Consumer recipe); - - @Nonnull - RigidBodySpawnBatchRecorder body(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - float positionX, - float positionY, - float positionZ, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode); - - @Nonnull - RigidBodySpawnBatchRecorder body(long bodyKeyMostSignificantBits, - long bodyKeyLeastSignificantBits, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull PhysicsBodyType bodyType, - float positionX, - float positionY, - float positionZ, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode); -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/RigidBodySpawnRecorder.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/RigidBodySpawnRecorder.java deleted file mode 100644 index 2ee4d985..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/RigidBodySpawnRecorder.java +++ /dev/null @@ -1,95 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.recorder; - -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import javax.annotation.Nonnull; - -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import org.joml.Vector3f; - -/** - * Fluent recorder for one rigid body spawn request. - */ -public interface RigidBodySpawnRecorder { - - @Nonnull - RigidBodySpawnRecorder space(@Nonnull SpaceId spaceId); - - @Nonnull - RigidBodySpawnRecorder box(float halfX, - float halfY, - float halfZ); - - @Nonnull - RigidBodySpawnRecorder shape(@Nonnull PhysicsShapeSpec shape); - - @Nonnull - RigidBodySpawnRecorder sphere(float radius); - - @Nonnull - RigidBodySpawnRecorder capsule(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis); - - @Nonnull - RigidBodySpawnRecorder cylinder(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis); - - @Nonnull - RigidBodySpawnRecorder cone(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis); - - @Nonnull - RigidBodySpawnRecorder plane(float groundY); - - @Nonnull - RigidBodySpawnRecorder mass(float mass); - - @Nonnull - RigidBodySpawnRecorder type(@Nonnull PhysicsBodyType bodyType); - - @Nonnull - RigidBodySpawnRecorder dynamic(); - - @Nonnull - RigidBodySpawnRecorder kinematic(); - - @Nonnull - RigidBodySpawnRecorder position(@Nonnull Vector3f position); - - @Nonnull - RigidBodySpawnRecorder position(float x, - float y, - float z); - - @Nonnull - RigidBodySpawnRecorder settings(@Nonnull RigidBodySpawnSettings settings); - - @Nonnull - RigidBodySpawnRecorder sensor(boolean sensor); - - @Nonnull - RigidBodySpawnRecorder collisionFilter(int group, - int mask); - - @Nonnull - RigidBodySpawnRecorder kind(@Nonnull PhysicsBodyKind kind); - - @Nonnull - RigidBodySpawnRecorder temporary(); - - @Nonnull - RigidBodySpawnRecorder persistence(@Nonnull PhysicsBodyPersistenceMode persistenceMode); - - @Nonnull - RigidBodySpawnRecorder runtimeOnly(); - - @Nonnull - RigidBodySpawnRecorder persistent(); -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/RigidBodySpawnTemplateRecorder.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/RigidBodySpawnTemplateRecorder.java deleted file mode 100644 index 2ace9ea2..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/recorder/RigidBodySpawnTemplateRecorder.java +++ /dev/null @@ -1,28 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.recorder; - -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import javax.annotation.Nonnull; -import org.joml.Vector3f; - -/** - * Fluent recorder for high-volume rigid body spawns sharing one spawn template. - */ -public interface RigidBodySpawnTemplateRecorder { - - @Nonnull - RigidBodySpawnTemplateRecorder body(@Nonnull RigidBodyKey bodyKey, - @Nonnull Vector3f position); - - @Nonnull - RigidBodySpawnTemplateRecorder body(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ); - - @Nonnull - RigidBodySpawnTemplateRecorder body(long bodyKeyMostSignificantBits, - long bodyKeyLeastSignificantBits, - float positionX, - float positionY, - float positionZ); -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/view/RaycastHitView.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/view/RaycastHitView.java deleted file mode 100644 index 12ffe4b5..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/view/RaycastHitView.java +++ /dev/null @@ -1,70 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.view; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import java.util.Objects; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3f; - -/** - * Copied raycast result that does not expose a live backend body. - */ -public record RaycastHitView(@Nullable RigidBodyKey bodyKey, - @Nonnull PhysicsBodyType bodyType, - float pointX, - float pointY, - float pointZ, - float normalX, - float normalY, - float normalZ, - @Nonnull ShapeType shapeType, - float fraction, - float distance) { - - public RaycastHitView(@Nullable RigidBodyKey bodyKey, - @Nonnull PhysicsBodyType bodyType, - @Nonnull Vector3f point, - @Nonnull Vector3f normal, - @Nonnull ShapeType shapeType, - float fraction, - float distance) { - this(bodyKey, - bodyType, - Objects.requireNonNull(point, "point").x, - point.y, - point.z, - Objects.requireNonNull(normal, "normal").x, - normal.y, - normal.z, - shapeType, - fraction, - distance); - } - - public RaycastHitView { - Objects.requireNonNull(bodyType, "bodyType"); - Objects.requireNonNull(shapeType, "shapeType"); - } - - @Nonnull - public Vector3f point() { - return new Vector3f(pointX, pointY, pointZ); - } - - @Nonnull - public Vector3f normal() { - return new Vector3f(normalX, normalY, normalZ); - } - - @Nonnull - public Vector3f copyPointTo(@Nonnull Vector3f target) { - return Objects.requireNonNull(target, "target").set(pointX, pointY, pointZ); - } - - @Nonnull - public Vector3f copyNormalTo(@Nonnull Vector3f target) { - return Objects.requireNonNull(target, "target").set(normalX, normalY, normalZ); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/view/RigidBodyStateView.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/view/RigidBodyStateView.java deleted file mode 100644 index 64f94cd3..00000000 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/simulation/view/RigidBodyStateView.java +++ /dev/null @@ -1,25 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation.view; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodyPose; - -import java.util.Objects; -import javax.annotation.Nonnull; - -/** - * Copied rigid body state returned by owner-lane queries. - * - *

This value is a live-state query result, not a published snapshot frame entry. Use snapshot - * APIs when reader-side systems need frame-coherent body data.

- */ -public record RigidBodyStateView(@Nonnull RigidBodyKey bodyKey, - @Nonnull PhysicsBodyType bodyType, - @Nonnull RigidBodyPose pose) { - - public RigidBodyStateView { - Objects.requireNonNull(bodyKey, "bodyKey"); - Objects.requireNonNull(bodyType, "bodyType"); - Objects.requireNonNull(pose, "pose"); - } -} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PhysicsBodySnapshotEntry.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PhysicsBodySnapshotEntry.java index ee30419f..715f6002 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PhysicsBodySnapshotEntry.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PhysicsBodySnapshotEntry.java @@ -2,18 +2,20 @@ import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; +import java.util.Objects; +import java.util.UUID; import javax.annotation.Nonnull; /** - * Snapshot query result carrying a body's stable key, latest snapshot, and registration metadata. + * Snapshot query result carrying a body's durable UUID, latest snapshot, and owning space. */ -public record PhysicsBodySnapshotEntry(@Nonnull RigidBodyKey bodyKey, +public record PhysicsBodySnapshotEntry(@Nonnull UUID bodyUuid, @Nonnull PhysicsBodySnapshot snapshot, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { + @Nonnull SpaceId spaceId) { + public PhysicsBodySnapshotEntry { + Objects.requireNonNull(bodyUuid, "bodyUuid"); + Objects.requireNonNull(snapshot, "snapshot"); + Objects.requireNonNull(spaceId, "spaceId"); + } } diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsBodyFrameStorage.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsBodyFrameStorage.java index dd7c6399..eb54944d 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsBodyFrameStorage.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsBodyFrameStorage.java @@ -5,10 +5,8 @@ import dev.hytalemodding.impulse.api.PhysicsBodyType; import dev.hytalemodding.impulse.api.ShapeType; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; import java.util.Objects; +import java.util.UUID; import java.util.function.Consumer; import javax.annotation.Nonnull; @@ -51,12 +49,11 @@ final class PublishedPhysicsBodyFrameStorage { private final long[] spaceEpochs; private final int[] spaceBodyStarts; private final int[] spaceBodyCounts; - private final RigidBodyKey[] bodyKeys; + private final long[] bodyUuidMostSignificantBits; + private final long[] bodyUuidLeastSignificantBits; private final SpaceId[] bodySpaceIds; private final long[] bodySpaceEpochs; private final long[] registrationGenerations; - private final PhysicsBodyKind[] kinds; - private final PhysicsBodyPersistenceMode[] persistenceModes; private final PhysicsBodyType[] bodyTypes; private final ShapeType[] shapeTypes; private final PhysicsAxis[] shapeAxes; @@ -71,12 +68,11 @@ private PublishedPhysicsBodyFrameStorage(long frameEpoch, long[] spaceEpochs, int[] spaceBodyStarts, int[] spaceBodyCounts, - RigidBodyKey[] bodyKeys, + long[] bodyUuidMostSignificantBits, + long[] bodyUuidLeastSignificantBits, SpaceId[] bodySpaceIds, long[] bodySpaceEpochs, long[] registrationGenerations, - PhysicsBodyKind[] kinds, - PhysicsBodyPersistenceMode[] persistenceModes, PhysicsBodyType[] bodyTypes, ShapeType[] shapeTypes, PhysicsAxis[] shapeAxes, @@ -90,12 +86,11 @@ private PublishedPhysicsBodyFrameStorage(long frameEpoch, this.spaceEpochs = spaceEpochs; this.spaceBodyStarts = spaceBodyStarts; this.spaceBodyCounts = spaceBodyCounts; - this.bodyKeys = bodyKeys; + this.bodyUuidMostSignificantBits = bodyUuidMostSignificantBits; + this.bodyUuidLeastSignificantBits = bodyUuidLeastSignificantBits; this.bodySpaceIds = bodySpaceIds; this.bodySpaceEpochs = bodySpaceEpochs; this.registrationGenerations = registrationGenerations; - this.kinds = kinds; - this.persistenceModes = persistenceModes; this.bodyTypes = bodyTypes; this.shapeTypes = shapeTypes; this.shapeAxes = shapeAxes; @@ -114,7 +109,7 @@ int spaceCount() { } int bodyCount() { - return bodyKeys.length; + return bodyUuidMostSignificantBits.length; } SpaceId spaceId(int spaceIndex) { @@ -134,14 +129,13 @@ int spaceBodyCount(int spaceIndex) { } PublishedPhysicsBodySnapshot bodySnapshot(int bodyIndex) { - return new PublishedPhysicsBodySnapshot(bodyKey(bodyIndex), + return new PublishedPhysicsBodySnapshot(bodyUuidMostSignificantBits(bodyIndex), + bodyUuidLeastSignificantBits(bodyIndex), bodySpaceId(bodyIndex), frameEpoch, worldEpoch, bodySpaceEpoch(bodyIndex), registrationGeneration(bodyIndex), - kind(bodyIndex), - persistenceMode(bodyIndex), positionX(bodyIndex), positionY(bodyIndex), positionZ(bodyIndex), @@ -180,14 +174,23 @@ PublishedPhysicsBodySnapshot bodySnapshot(int bodyIndex) { void forEachBodyCursor(@Nonnull Consumer consumer) { Objects.requireNonNull(consumer, "consumer"); FrameBodyCursor cursor = new FrameBodyCursor(); - for (int bodyIndex = 0; bodyIndex < bodyKeys.length; bodyIndex++) { + for (int bodyIndex = 0; bodyIndex < bodyUuidMostSignificantBits.length; bodyIndex++) { cursor.index = bodyIndex; consumer.accept(cursor); } } - private RigidBodyKey bodyKey(int bodyIndex) { - return bodyKeys[bodyIndex]; + private UUID bodyUuid(int bodyIndex) { + return new UUID(bodyUuidMostSignificantBits(bodyIndex), + bodyUuidLeastSignificantBits(bodyIndex)); + } + + private long bodyUuidMostSignificantBits(int bodyIndex) { + return bodyUuidMostSignificantBits[bodyIndex]; + } + + private long bodyUuidLeastSignificantBits(int bodyIndex) { + return bodyUuidLeastSignificantBits[bodyIndex]; } private SpaceId bodySpaceId(int bodyIndex) { @@ -202,14 +205,6 @@ private long registrationGeneration(int bodyIndex) { return registrationGenerations[bodyIndex]; } - private PhysicsBodyKind kind(int bodyIndex) { - return kinds[bodyIndex]; - } - - private PhysicsBodyPersistenceMode persistenceMode(int bodyIndex) { - return persistenceModes[bodyIndex]; - } - private PhysicsBodyType bodyType(int bodyIndex) { return bodyTypes[bodyIndex]; } @@ -354,12 +349,11 @@ static final class Builder { private final long[] spaceEpochs; private final int[] spaceBodyStarts; private final int[] spaceBodyCounts; - private final RigidBodyKey[] bodyKeys; + private final long[] bodyUuidMostSignificantBits; + private final long[] bodyUuidLeastSignificantBits; private final SpaceId[] bodySpaceIds; private final long[] bodySpaceEpochs; private final long[] registrationGenerations; - private final PhysicsBodyKind[] kinds; - private final PhysicsBodyPersistenceMode[] persistenceModes; private final PhysicsBodyType[] bodyTypes; private final ShapeType[] shapeTypes; private final PhysicsAxis[] shapeAxes; @@ -385,12 +379,11 @@ private Builder(long frameEpoch, long worldEpoch, int expectedSpaces, int expect this.spaceEpochs = new long[expectedSpaces]; this.spaceBodyStarts = new int[expectedSpaces]; this.spaceBodyCounts = new int[expectedSpaces]; - this.bodyKeys = new RigidBodyKey[expectedBodies]; + this.bodyUuidMostSignificantBits = new long[expectedBodies]; + this.bodyUuidLeastSignificantBits = new long[expectedBodies]; this.bodySpaceIds = new SpaceId[expectedBodies]; this.bodySpaceEpochs = new long[expectedBodies]; this.registrationGenerations = new long[expectedBodies]; - this.kinds = new PhysicsBodyKind[expectedBodies]; - this.persistenceModes = new PhysicsBodyPersistenceMode[expectedBodies]; this.bodyTypes = new PhysicsBodyType[expectedBodies]; this.shapeTypes = new ShapeType[expectedBodies]; this.shapeAxes = new PhysicsAxis[expectedBodies]; @@ -417,12 +410,25 @@ void addSpace(@Nonnull SpaceId spaceId, long spaceEpoch, int bodyCount) { nextSpace++; } - void addBody(@Nonnull RigidBodyKey bodyKey, + void addBody(@Nonnull UUID bodyUuid, + @Nonnull SpaceId spaceId, + long spaceEpoch, + long registrationGeneration, + @Nonnull PhysicsBodySnapshot snapshot) { + Objects.requireNonNull(bodyUuid, "bodyUuid"); + addBody(bodyUuid.getMostSignificantBits(), + bodyUuid.getLeastSignificantBits(), + spaceId, + spaceEpoch, + registrationGeneration, + snapshot); + } + + private void addBody(long bodyUuidMostSignificantBits, + long bodyUuidLeastSignificantBits, @Nonnull SpaceId spaceId, long spaceEpoch, long registrationGeneration, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode, @Nonnull PhysicsBodySnapshot snapshot) { Objects.requireNonNull(snapshot, "snapshot"); if (currentSpace < 0) { @@ -434,15 +440,14 @@ void addBody(@Nonnull RigidBodyKey bodyKey, if (currentSpaceBodyCount >= spaceBodyCounts[currentSpace]) { throw new IllegalStateException("too many bodies added to current published space frame"); } - if (nextBody >= bodyKeys.length) { + if (nextBody >= this.bodyUuidMostSignificantBits.length) { throw new IllegalStateException("too many bodies added to published frame"); } - bodyKeys[nextBody] = Objects.requireNonNull(bodyKey, "bodyKey"); + this.bodyUuidMostSignificantBits[nextBody] = bodyUuidMostSignificantBits; + this.bodyUuidLeastSignificantBits[nextBody] = bodyUuidLeastSignificantBits; bodySpaceIds[nextBody] = Objects.requireNonNull(spaceId, "spaceId"); bodySpaceEpochs[nextBody] = spaceEpoch; registrationGenerations[nextBody] = registrationGeneration; - kinds[nextBody] = Objects.requireNonNull(kind, "kind"); - persistenceModes[nextBody] = Objects.requireNonNull(persistenceMode, "persistenceMode"); bodyTypes[nextBody] = snapshot.bodyType(); shapeTypes[nextBody] = snapshot.shapeType(); shapeAxes[nextBody] = snapshot.shapeAxis(); @@ -495,7 +500,7 @@ PublishedPhysicsBodyFrameStorage build() { if (nextSpace != spaceIds.length) { throw new IllegalStateException("published frame space count mismatch"); } - if (nextBody != bodyKeys.length) { + if (nextBody != bodyUuidMostSignificantBits.length) { throw new IllegalStateException("published frame body count mismatch"); } return new PublishedPhysicsBodyFrameStorage(frameEpoch, @@ -504,12 +509,11 @@ PublishedPhysicsBodyFrameStorage build() { spaceEpochs, spaceBodyStarts, spaceBodyCounts, - bodyKeys, + bodyUuidMostSignificantBits, + bodyUuidLeastSignificantBits, bodySpaceIds, bodySpaceEpochs, registrationGenerations, - kinds, - persistenceModes, bodyTypes, shapeTypes, shapeAxes, @@ -536,8 +540,8 @@ private final class FrameBodyCursor implements PublishedPhysicsBodySnapshotCurso @Nonnull @Override - public RigidBodyKey bodyKey() { - return PublishedPhysicsBodyFrameStorage.this.bodyKey(index); + public UUID bodyUuid() { + return PublishedPhysicsBodyFrameStorage.this.bodyUuid(index); } @Nonnull @@ -566,18 +570,6 @@ public long registrationGeneration() { return PublishedPhysicsBodyFrameStorage.this.registrationGeneration(index); } - @Nonnull - @Override - public PhysicsBodyKind kind() { - return PublishedPhysicsBodyFrameStorage.this.kind(index); - } - - @Nonnull - @Override - public PhysicsBodyPersistenceMode persistenceMode() { - return PublishedPhysicsBodyFrameStorage.this.persistenceMode(index); - } - @Override public float positionX() { return PublishedPhysicsBodyFrameStorage.this.positionX(index); diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsBodySnapshot.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsBodySnapshot.java index 1b9bf6bd..9a8f4dbf 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsBodySnapshot.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsBodySnapshot.java @@ -5,10 +5,8 @@ import dev.hytalemodding.impulse.api.PhysicsAxis; import dev.hytalemodding.impulse.api.ShapeType; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; import java.util.Objects; +import java.util.UUID; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.joml.Quaternionf; @@ -17,23 +15,19 @@ /** * Immutable body state published as part of an async snapshot frame. * - *

This type deliberately carries an Impulse body key instead of backend - * body handles so published frames can be read away from the owner lane.

+ *

This type deliberately carries a durable body UUID instead of backend + * body handles so published frames can be read away from the store tick lane.

*/ public final class PublishedPhysicsBodySnapshot implements PublishedPhysicsBodySnapshotCursor { - @Nonnull - private final RigidBodyKey bodyKey; + private final long bodyUuidMostSignificantBits; + private final long bodyUuidLeastSignificantBits; @Nonnull private final SpaceId spaceId; private final long frameEpoch; private final long worldEpoch; private final long spaceEpoch; private final long registrationGeneration; - @Nonnull - private final PhysicsBodyKind kind; - @Nonnull - private final PhysicsBodyPersistenceMode persistenceMode; private final float positionX; private final float positionY; private final float positionZ; @@ -71,14 +65,12 @@ public final class PublishedPhysicsBodySnapshot implements PublishedPhysicsBodyS @Nonnull private final PhysicsAxis shapeAxis; - public PublishedPhysicsBodySnapshot(@Nonnull RigidBodyKey bodyKey, + public PublishedPhysicsBodySnapshot(@Nonnull UUID bodyUuid, @Nonnull SpaceId spaceId, long frameEpoch, long worldEpoch, long spaceEpoch, long registrationGeneration, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode, @Nonnull Vector3f position, @Nonnull Quaternionf rotation, @Nonnull Vector3f linearVelocity, @@ -92,7 +84,50 @@ public PublishedPhysicsBodySnapshot(@Nonnull RigidBodyKey bodyKey, float sphereRadius, float halfHeight, @Nonnull PhysicsAxis shapeAxis) { - this.bodyKey = Objects.requireNonNull(bodyKey, "bodyKey"); + this(uuidMostSignificantBits(bodyUuid), + uuidLeastSignificantBits(bodyUuid), + spaceId, + frameEpoch, + worldEpoch, + spaceEpoch, + registrationGeneration, + position, + rotation, + linearVelocity, + angularVelocity, + bodyType, + sleeping, + sensor, + centerOfMassOffsetY, + shapeType, + boxHalfExtents, + sphereRadius, + halfHeight, + shapeAxis); + } + + private PublishedPhysicsBodySnapshot(long bodyUuidMostSignificantBits, + long bodyUuidLeastSignificantBits, + @Nonnull SpaceId spaceId, + long frameEpoch, + long worldEpoch, + long spaceEpoch, + long registrationGeneration, + @Nonnull Vector3f position, + @Nonnull Quaternionf rotation, + @Nonnull Vector3f linearVelocity, + @Nonnull Vector3f angularVelocity, + @Nonnull PhysicsBodyType bodyType, + boolean sleeping, + boolean sensor, + float centerOfMassOffsetY, + @Nonnull ShapeType shapeType, + @Nullable Vector3f boxHalfExtents, + float sphereRadius, + float halfHeight, + @Nonnull PhysicsAxis shapeAxis) { + this.bodyUuidMostSignificantBits = bodyUuidMostSignificantBits; + this.bodyUuidLeastSignificantBits = bodyUuidLeastSignificantBits; this.spaceId = Objects.requireNonNull(spaceId, "spaceId"); requireNonNegativeEpoch(frameEpoch, "frameEpoch"); requireNonNegativeEpoch(worldEpoch, "worldEpoch"); @@ -102,8 +137,6 @@ public PublishedPhysicsBodySnapshot(@Nonnull RigidBodyKey bodyKey, this.worldEpoch = worldEpoch; this.spaceEpoch = spaceEpoch; this.registrationGeneration = registrationGeneration; - this.kind = Objects.requireNonNull(kind, "kind"); - this.persistenceMode = Objects.requireNonNull(persistenceMode, "persistenceMode"); Objects.requireNonNull(position, "position"); this.positionX = position.x; this.positionY = position.y; @@ -151,24 +184,39 @@ public PublishedPhysicsBodySnapshot(@Nonnull RigidBodyKey bodyKey, } @Nonnull - public static PublishedPhysicsBodySnapshot from(@Nonnull RigidBodyKey bodyKey, + public static PublishedPhysicsBodySnapshot from(@Nonnull UUID bodyUuid, + @Nonnull SpaceId spaceId, + long frameEpoch, + long worldEpoch, + long spaceEpoch, + long registrationGeneration, + @Nonnull PhysicsBodySnapshot snapshot) { + return fromBits(uuidMostSignificantBits(bodyUuid), + uuidLeastSignificantBits(bodyUuid), + spaceId, + frameEpoch, + worldEpoch, + spaceEpoch, + registrationGeneration, + snapshot); + } + + private static PublishedPhysicsBodySnapshot fromBits(long bodyUuidMostSignificantBits, + long bodyUuidLeastSignificantBits, @Nonnull SpaceId spaceId, long frameEpoch, long worldEpoch, long spaceEpoch, long registrationGeneration, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode, @Nonnull PhysicsBodySnapshot snapshot) { Objects.requireNonNull(snapshot, "snapshot"); - return new PublishedPhysicsBodySnapshot(bodyKey, + return new PublishedPhysicsBodySnapshot(bodyUuidMostSignificantBits, + bodyUuidLeastSignificantBits, spaceId, frameEpoch, worldEpoch, spaceEpoch, registrationGeneration, - kind, - persistenceMode, snapshot.positionX(), snapshot.positionY(), snapshot.positionZ(), @@ -204,14 +252,21 @@ public static PublishedPhysicsBodySnapshot from(@Nonnull RigidBodyKey bodyKey, snapshot.shapeAxis()); } - PublishedPhysicsBodySnapshot(@Nonnull RigidBodyKey bodyKey, + private static long uuidMostSignificantBits(@Nonnull UUID bodyUuid) { + return Objects.requireNonNull(bodyUuid, "bodyUuid").getMostSignificantBits(); + } + + private static long uuidLeastSignificantBits(@Nonnull UUID bodyUuid) { + return Objects.requireNonNull(bodyUuid, "bodyUuid").getLeastSignificantBits(); + } + + PublishedPhysicsBodySnapshot(long bodyUuidMostSignificantBits, + long bodyUuidLeastSignificantBits, @Nonnull SpaceId spaceId, long frameEpoch, long worldEpoch, long spaceEpoch, long registrationGeneration, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode, float positionX, float positionY, float positionZ, @@ -245,7 +300,8 @@ public static PublishedPhysicsBodySnapshot from(@Nonnull RigidBodyKey bodyKey, float sphereRadius, float halfHeight, @Nonnull PhysicsAxis shapeAxis) { - this.bodyKey = Objects.requireNonNull(bodyKey, "bodyKey"); + this.bodyUuidMostSignificantBits = bodyUuidMostSignificantBits; + this.bodyUuidLeastSignificantBits = bodyUuidLeastSignificantBits; this.spaceId = Objects.requireNonNull(spaceId, "spaceId"); requireNonNegativeEpoch(frameEpoch, "frameEpoch"); requireNonNegativeEpoch(worldEpoch, "worldEpoch"); @@ -255,8 +311,6 @@ public static PublishedPhysicsBodySnapshot from(@Nonnull RigidBodyKey bodyKey, this.worldEpoch = worldEpoch; this.spaceEpoch = spaceEpoch; this.registrationGeneration = registrationGeneration; - this.kind = Objects.requireNonNull(kind, "kind"); - this.persistenceMode = Objects.requireNonNull(persistenceMode, "persistenceMode"); this.positionX = positionX; this.positionY = positionY; this.positionZ = positionZ; @@ -379,8 +433,8 @@ public boolean matchesSnapshot(@Nonnull PhysicsBodySnapshot snapshot) { @Nonnull @Override - public RigidBodyKey bodyKey() { - return bodyKey; + public UUID bodyUuid() { + return new UUID(bodyUuidMostSignificantBits, bodyUuidLeastSignificantBits); } @Nonnull @@ -409,18 +463,6 @@ public long registrationGeneration() { return registrationGeneration; } - @Nonnull - @Override - public PhysicsBodyKind kind() { - return kind; - } - - @Nonnull - @Override - public PhysicsBodyPersistenceMode persistenceMode() { - return persistenceMode; - } - @Nonnull public Vector3f position() { return new Vector3f(positionX, positionY, positionZ); @@ -704,10 +746,9 @@ public boolean equals(@Nullable Object other) { && Float.compare(boxHalfExtentZ, that.boxHalfExtentZ) == 0 && Float.compare(sphereRadius, that.sphereRadius) == 0 && Float.compare(halfHeight, that.halfHeight) == 0 - && bodyKey.equals(that.bodyKey) + && bodyUuidMostSignificantBits == that.bodyUuidMostSignificantBits + && bodyUuidLeastSignificantBits == that.bodyUuidLeastSignificantBits && spaceId.equals(that.spaceId) - && kind == that.kind - && persistenceMode == that.persistenceMode && bodyType == that.bodyType && shapeType == that.shapeType && shapeAxis == that.shapeAxis; @@ -715,14 +756,13 @@ public boolean equals(@Nullable Object other) { @Override public int hashCode() { - int result = bodyKey.hashCode(); + int result = Long.hashCode(bodyUuidMostSignificantBits); + result = 31 * result + Long.hashCode(bodyUuidLeastSignificantBits); result = 31 * result + spaceId.hashCode(); result = 31 * result + Long.hashCode(frameEpoch); result = 31 * result + Long.hashCode(worldEpoch); result = 31 * result + Long.hashCode(spaceEpoch); result = 31 * result + Long.hashCode(registrationGeneration); - result = 31 * result + kind.hashCode(); - result = 31 * result + persistenceMode.hashCode(); result = 31 * result + Float.hashCode(positionX); result = 31 * result + Float.hashCode(positionY); result = 31 * result + Float.hashCode(positionZ); @@ -763,14 +803,12 @@ public int hashCode() { @Override public String toString() { return "PublishedPhysicsBodySnapshot[" - + "bodyKey=" + bodyKey + + "bodyUuid=" + bodyUuid() + ", spaceId=" + spaceId + ", frameEpoch=" + frameEpoch + ", worldEpoch=" + worldEpoch + ", spaceEpoch=" + spaceEpoch + ", registrationGeneration=" + registrationGeneration - + ", kind=" + kind - + ", persistenceMode=" + persistenceMode + ", position=(" + positionX + ", " + positionY + ", " + positionZ + ')' + ", rotation=(" + rotationX + ", " + rotationY + ", " + rotationZ + ", " + rotationW + ')' + ", linearVelocity=(" + linearVelocityX + ", " + linearVelocityY + ", " + linearVelocityZ + ')' diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsBodySnapshotCursor.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsBodySnapshotCursor.java index 3fbc169f..455f29f0 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsBodySnapshotCursor.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsBodySnapshotCursor.java @@ -5,9 +5,7 @@ import dev.hytalemodding.impulse.api.PhysicsBodyType; import dev.hytalemodding.impulse.api.ShapeType; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; +import java.util.UUID; import javax.annotation.Nonnull; import org.joml.Quaternionf; import org.joml.Vector3f; @@ -22,7 +20,7 @@ public interface PublishedPhysicsBodySnapshotCursor { @Nonnull - RigidBodyKey bodyKey(); + UUID bodyUuid(); @Nonnull SpaceId spaceId(); @@ -35,12 +33,6 @@ public interface PublishedPhysicsBodySnapshotCursor { long registrationGeneration(); - @Nonnull - PhysicsBodyKind kind(); - - @Nonnull - PhysicsBodyPersistenceMode persistenceMode(); - float positionX(); float positionY(); diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsSnapshotFrame.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsSnapshotFrame.java index 65074814..22d2dacd 100644 --- a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsSnapshotFrame.java +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsSnapshotFrame.java @@ -2,12 +2,10 @@ import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.UUID; import java.util.function.Consumer; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -32,24 +30,18 @@ public enum Status { private final long worldEpoch; /** - * Impulse step-scheduler sequence assigned to the owner step + * Impulse step-scheduler sequence assigned to the store tick step * not a Hytale tick and not guaranteed contiguous in published frames */ private final long stepSequence; /** - * Hytale world tick observed when the owner step was scheduled. + * Hytale world tick observed when the store tick step was scheduled. * This is not a physics step counter and may diverge from {@code stepSequence} under paused * worlds, backpressure, or future multi-rate scheduling */ private final long serverTick; - /** - * Latest command-batch sequence whose owner-lane execution completed before this frame was - * captured. - * Command completion can precede inclusion in a later captured frame. - */ - private final long lastIncludedCommandBatchSequence; @Nonnull private final Status status; private final int spatialIndexCellCount; @@ -73,30 +65,6 @@ public PublishedPhysicsSnapshotFrame(long frameEpoch, worldEpoch, stepSequence, serverTick, - 0L, - status, - spatialIndexCellCount, - stepNanos, - snapshotNanos, - null, - copyAndValidateSpaces(frameEpoch, worldEpoch, spaces)); - } - - public PublishedPhysicsSnapshotFrame(long frameEpoch, - long worldEpoch, - long stepSequence, - long serverTick, - long lastIncludedCommandBatchSequence, - @Nonnull Status status, - int spatialIndexCellCount, - long stepNanos, - long snapshotNanos, - @Nonnull List spaces) { - this(frameEpoch, - worldEpoch, - stepSequence, - serverTick, - lastIncludedCommandBatchSequence, status, spatialIndexCellCount, stepNanos, @@ -109,7 +77,6 @@ private PublishedPhysicsSnapshotFrame(long frameEpoch, long worldEpoch, long stepSequence, long serverTick, - long lastIncludedCommandBatchSequence, @Nonnull Status status, int spatialIndexCellCount, long stepNanos, @@ -119,7 +86,6 @@ private PublishedPhysicsSnapshotFrame(long frameEpoch, worldEpoch, stepSequence, serverTick, - lastIncludedCommandBatchSequence, status, spatialIndexCellCount, stepNanos, @@ -132,7 +98,6 @@ private PublishedPhysicsSnapshotFrame(long frameEpoch, long worldEpoch, long stepSequence, long serverTick, - long lastIncludedCommandBatchSequence, @Nonnull Status status, int spatialIndexCellCount, long stepNanos, @@ -143,7 +108,6 @@ private PublishedPhysicsSnapshotFrame(long frameEpoch, worldEpoch, stepSequence, serverTick, - lastIncludedCommandBatchSequence, status, spatialIndexCellCount, stepNanos, @@ -152,7 +116,6 @@ private PublishedPhysicsSnapshotFrame(long frameEpoch, this.worldEpoch = worldEpoch; this.stepSequence = stepSequence; this.serverTick = serverTick; - this.lastIncludedCommandBatchSequence = lastIncludedCommandBatchSequence; this.status = status; this.spatialIndexCellCount = spatialIndexCellCount; this.stepNanos = stepNanos; @@ -167,7 +130,6 @@ public static PublishedPhysicsSnapshotFrame empty(long frameEpoch, long worldEpo worldEpoch, 0L, 0L, - 0L, Status.EMPTY, 0, 0L, @@ -198,31 +160,6 @@ public static Builder compactBuilder(long frameEpoch, expectedBodies); } - @Nonnull - public static Builder compactBuilder(long frameEpoch, - long worldEpoch, - long stepSequence, - long serverTick, - long lastIncludedCommandBatchSequence, - @Nonnull Status status, - int spatialIndexCellCount, - long stepNanos, - long snapshotNanos, - int expectedSpaces, - int expectedBodies) { - return new Builder(frameEpoch, - worldEpoch, - stepSequence, - serverTick, - lastIncludedCommandBatchSequence, - status, - spatialIndexCellCount, - stepNanos, - snapshotNanos, - expectedSpaces, - expectedBodies); - } - public long frameEpoch() { return frameEpoch; } @@ -239,25 +176,8 @@ public long serverTick() { return serverTick; } - public long lastIncludedCommandBatchSequence() { - return lastIncludedCommandBatchSequence; - } - /** - * Returns whether this snapshot frame is known to include owner-lane execution of the - * submitted command batch sequence. - * - *

This is the latest owner-executed command-batch sequence included by the captured - * snapshot. It does not mean a separate ECS visual sync pass has already consumed the frame. - * Batches at or below this sequence are included in this frame's copied body data.

- */ - public boolean includesCommandBatch(long commandBatchSequence) { - return commandBatchSequence > 0L - && lastIncludedCommandBatchSequence >= commandBatchSequence; - } - - /** - * Returns the server-tick distance from a submitted command to this snapshot frame, or + * Returns the server-tick distance from a submitted action to this snapshot frame, or * {@code 0} when this frame has no usable server-tick metadata. */ public long serverTickLatencyFromSubmittedTick(long submittedServerTick) { @@ -367,7 +287,6 @@ public boolean equals(@Nullable Object other) { && worldEpoch == that.worldEpoch && stepSequence == that.stepSequence && serverTick == that.serverTick - && lastIncludedCommandBatchSequence == that.lastIncludedCommandBatchSequence && spatialIndexCellCount == that.spatialIndexCellCount && stepNanos == that.stepNanos && snapshotNanos == that.snapshotNanos @@ -381,7 +300,6 @@ public int hashCode() { result = 31 * result + Long.hashCode(worldEpoch); result = 31 * result + Long.hashCode(stepSequence); result = 31 * result + Long.hashCode(serverTick); - result = 31 * result + Long.hashCode(lastIncludedCommandBatchSequence); result = 31 * result + status.hashCode(); result = 31 * result + Integer.hashCode(spatialIndexCellCount); result = 31 * result + Long.hashCode(stepNanos); @@ -397,7 +315,6 @@ public String toString() { + ", worldEpoch=" + worldEpoch + ", stepSequence=" + stepSequence + ", serverTick=" + serverTick - + ", lastIncludedCommandBatchSequence=" + lastIncludedCommandBatchSequence + ", status=" + status + ", spatialIndexCellCount=" + spatialIndexCellCount + ", stepNanos=" + stepNanos @@ -443,7 +360,6 @@ private static void requireFrameValues(long frameEpoch, long worldEpoch, long stepSequence, long serverTick, - long lastIncludedCommandBatchSequence, @Nonnull Status status, int spatialIndexCellCount, long stepNanos, @@ -452,7 +368,6 @@ private static void requireFrameValues(long frameEpoch, requireNonNegativeEpoch(worldEpoch, "worldEpoch"); requireNonNegativeEpoch(stepSequence, "stepSequence"); requireNonNegativeEpoch(serverTick, "serverTick"); - requireNonNegativeEpoch(lastIncludedCommandBatchSequence, "lastIncludedCommandBatchSequence"); Objects.requireNonNull(status, "status"); if (spatialIndexCellCount < 0) { throw new IllegalArgumentException("spatialIndexCellCount cannot be negative"); @@ -484,7 +399,6 @@ public static final class Builder { private final long worldEpoch; private final long stepSequence; private final long serverTick; - private final long lastIncludedCommandBatchSequence; @Nonnull private final Status status; private final int spatialIndexCellCount; @@ -502,35 +416,10 @@ private Builder(long frameEpoch, long snapshotNanos, int expectedSpaces, int expectedBodies) { - this(frameEpoch, - worldEpoch, - stepSequence, - serverTick, - 0L, - status, - spatialIndexCellCount, - stepNanos, - snapshotNanos, - expectedSpaces, - expectedBodies); - } - - private Builder(long frameEpoch, - long worldEpoch, - long stepSequence, - long serverTick, - long lastIncludedCommandBatchSequence, - @Nonnull Status status, - int spatialIndexCellCount, - long stepNanos, - long snapshotNanos, - int expectedSpaces, - int expectedBodies) { requireFrameValues(frameEpoch, worldEpoch, stepSequence, serverTick, - lastIncludedCommandBatchSequence, status, spatialIndexCellCount, stepNanos, @@ -539,7 +428,6 @@ private Builder(long frameEpoch, this.worldEpoch = worldEpoch; this.stepSequence = stepSequence; this.serverTick = serverTick; - this.lastIncludedCommandBatchSequence = lastIncludedCommandBatchSequence; this.status = status; this.spatialIndexCellCount = spatialIndexCellCount; this.stepNanos = stepNanos; @@ -557,19 +445,15 @@ public Builder addSpace(@Nonnull SpaceId spaceId, long spaceEpoch, int bodyCount } @Nonnull - public Builder addBody(@Nonnull RigidBodyKey bodyKey, + public Builder addBody(@Nonnull UUID bodyUuid, @Nonnull SpaceId spaceId, long spaceEpoch, long registrationGeneration, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode, @Nonnull PhysicsBodySnapshot snapshot) { - bodyStorage.addBody(bodyKey, + bodyStorage.addBody(bodyUuid, spaceId, spaceEpoch, registrationGeneration, - kind, - persistenceMode, snapshot); return this; } @@ -580,7 +464,6 @@ public PublishedPhysicsSnapshotFrame build() { worldEpoch, stepSequence, serverTick, - lastIncludedCommandBatchSequence, status, spatialIndexCellCount, stepNanos, diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshots/PhysicsBodySnapshot.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshots/PhysicsBodySnapshot.java new file mode 100644 index 00000000..5d17dfa5 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshots/PhysicsBodySnapshot.java @@ -0,0 +1,405 @@ +package dev.hytalemodding.impulse.core.plugin.snapshots; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Quaternionf; +import org.joml.Vector3f; + +/** + * Copied body snapshot published out of PhysicsStore for projection and queries. + */ +public final class PhysicsBodySnapshot { + + @Nullable + private final Ref bodyRef; + @Nonnull + private final UUID bodyUuid; + @Nonnull + private final UUID spaceUuid; + @Nonnull + private final PhysicsBodyType bodyType; + private final float positionX; + private final float positionY; + private final float positionZ; + private final float rotationX; + private final float rotationY; + private final float rotationZ; + private final float rotationW; + private final float linearVelocityX; + private final float linearVelocityY; + private final float linearVelocityZ; + private final float angularVelocityX; + private final float angularVelocityY; + private final float angularVelocityZ; + private final float centerOfMassOffsetY; + private final boolean sleeping; + + public PhysicsBodySnapshot(@Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsBodyType bodyType, + @Nonnull Vector3f position, + @Nonnull Quaternionf rotation, + @Nonnull Vector3f linearVelocity, + @Nonnull Vector3f angularVelocity, + float centerOfMassOffsetY, + boolean sleeping) { + this(null, + bodyUuid, + spaceUuid, + bodyType, + position, + rotation, + linearVelocity, + angularVelocity, + centerOfMassOffsetY, + sleeping); + } + + public PhysicsBodySnapshot(@Nullable Ref bodyRef, + @Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsBodyType bodyType, + @Nonnull Vector3f position, + @Nonnull Quaternionf rotation, + @Nonnull Vector3f linearVelocity, + @Nonnull Vector3f angularVelocity, + float centerOfMassOffsetY, + boolean sleeping) { + this(bodyRef, + bodyUuid, + spaceUuid, + bodyType, + Objects.requireNonNull(position, "position").x, + position.y, + position.z, + Objects.requireNonNull(rotation, "rotation").x, + rotation.y, + rotation.z, + rotation.w, + Objects.requireNonNull(linearVelocity, "linearVelocity").x, + linearVelocity.y, + linearVelocity.z, + Objects.requireNonNull(angularVelocity, "angularVelocity").x, + angularVelocity.y, + angularVelocity.z, + centerOfMassOffsetY, + sleeping); + } + + @Nonnull + public static PhysicsBodySnapshot of(@Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsBodyType bodyType, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW, + float linearVelocityX, + float linearVelocityY, + float linearVelocityZ, + float angularVelocityX, + float angularVelocityY, + float angularVelocityZ, + float centerOfMassOffsetY, + boolean sleeping) { + return of(null, + bodyUuid, + spaceUuid, + bodyType, + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW, + linearVelocityX, + linearVelocityY, + linearVelocityZ, + angularVelocityX, + angularVelocityY, + angularVelocityZ, + centerOfMassOffsetY, + sleeping); + } + + @Nonnull + public static PhysicsBodySnapshot of(@Nullable Ref bodyRef, + @Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsBodyType bodyType, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW, + float linearVelocityX, + float linearVelocityY, + float linearVelocityZ, + float angularVelocityX, + float angularVelocityY, + float angularVelocityZ, + float centerOfMassOffsetY, + boolean sleeping) { + return new PhysicsBodySnapshot(bodyRef, + bodyUuid, + spaceUuid, + bodyType, + positionX, + positionY, + positionZ, + rotationX, + rotationY, + rotationZ, + rotationW, + linearVelocityX, + linearVelocityY, + linearVelocityZ, + angularVelocityX, + angularVelocityY, + angularVelocityZ, + centerOfMassOffsetY, + sleeping); + } + + private PhysicsBodySnapshot(@Nullable Ref bodyRef, + @Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsBodyType bodyType, + float positionX, + float positionY, + float positionZ, + float rotationX, + float rotationY, + float rotationZ, + float rotationW, + float linearVelocityX, + float linearVelocityY, + float linearVelocityZ, + float angularVelocityX, + float angularVelocityY, + float angularVelocityZ, + float centerOfMassOffsetY, + boolean sleeping) { + this.bodyRef = bodyRef; + this.bodyUuid = Objects.requireNonNull(bodyUuid, "bodyUuid"); + this.spaceUuid = Objects.requireNonNull(spaceUuid, "spaceUuid"); + this.bodyType = Objects.requireNonNull(bodyType, "bodyType"); + this.positionX = positionX; + this.positionY = positionY; + this.positionZ = positionZ; + this.rotationX = rotationX; + this.rotationY = rotationY; + this.rotationZ = rotationZ; + this.rotationW = rotationW; + this.linearVelocityX = linearVelocityX; + this.linearVelocityY = linearVelocityY; + this.linearVelocityZ = linearVelocityZ; + this.angularVelocityX = angularVelocityX; + this.angularVelocityY = angularVelocityY; + this.angularVelocityZ = angularVelocityZ; + this.centerOfMassOffsetY = centerOfMassOffsetY; + this.sleeping = sleeping; + } + + @Nullable + public Ref bodyRef() { + return bodyRef; + } + + @Nonnull + public UUID bodyUuid() { + return bodyUuid; + } + + @Nonnull + public UUID spaceUuid() { + return spaceUuid; + } + + @Nonnull + public PhysicsBodyType bodyType() { + return bodyType; + } + + public float positionX() { + return positionX; + } + + public float positionY() { + return positionY; + } + + public float positionZ() { + return positionZ; + } + + public float rotationX() { + return rotationX; + } + + public float rotationY() { + return rotationY; + } + + public float rotationZ() { + return rotationZ; + } + + public float rotationW() { + return rotationW; + } + + public float linearVelocityX() { + return linearVelocityX; + } + + public float linearVelocityY() { + return linearVelocityY; + } + + public float linearVelocityZ() { + return linearVelocityZ; + } + + public float angularVelocityX() { + return angularVelocityX; + } + + public float angularVelocityY() { + return angularVelocityY; + } + + public float angularVelocityZ() { + return angularVelocityZ; + } + + public float centerOfMassOffsetY() { + return centerOfMassOffsetY; + } + + public boolean sleeping() { + return sleeping; + } + + @Nonnull + public Vector3f position() { + return copyPositionTo(new Vector3f()); + } + + @Nonnull + public Quaternionf rotation() { + return copyRotationTo(new Quaternionf()); + } + + @Nonnull + public Vector3f linearVelocity() { + return copyLinearVelocityTo(new Vector3f()); + } + + @Nonnull + public Vector3f angularVelocity() { + return copyAngularVelocityTo(new Vector3f()); + } + + @Nonnull + public Vector3f copyPositionTo(@Nonnull Vector3f target) { + return Objects.requireNonNull(target, "target").set(positionX, positionY, positionZ); + } + + @Nonnull + public Quaternionf copyRotationTo(@Nonnull Quaternionf target) { + return Objects.requireNonNull(target, "target") + .set(rotationX, rotationY, rotationZ, rotationW); + } + + @Nonnull + public Vector3f copyLinearVelocityTo(@Nonnull Vector3f target) { + return Objects.requireNonNull(target, "target") + .set(linearVelocityX, linearVelocityY, linearVelocityZ); + } + + @Nonnull + public Vector3f copyAngularVelocityTo(@Nonnull Vector3f target) { + return Objects.requireNonNull(target, "target") + .set(angularVelocityX, angularVelocityY, angularVelocityZ); + } + + @Override + public boolean equals(@Nullable Object other) { + if (this == other) { + return true; + } + if (!(other instanceof PhysicsBodySnapshot that)) { + return false; + } + return Float.compare(positionX, that.positionX) == 0 + && Float.compare(positionY, that.positionY) == 0 + && Float.compare(positionZ, that.positionZ) == 0 + && Float.compare(rotationX, that.rotationX) == 0 + && Float.compare(rotationY, that.rotationY) == 0 + && Float.compare(rotationZ, that.rotationZ) == 0 + && Float.compare(rotationW, that.rotationW) == 0 + && Float.compare(linearVelocityX, that.linearVelocityX) == 0 + && Float.compare(linearVelocityY, that.linearVelocityY) == 0 + && Float.compare(linearVelocityZ, that.linearVelocityZ) == 0 + && Float.compare(angularVelocityX, that.angularVelocityX) == 0 + && Float.compare(angularVelocityY, that.angularVelocityY) == 0 + && Float.compare(angularVelocityZ, that.angularVelocityZ) == 0 + && Float.compare(centerOfMassOffsetY, that.centerOfMassOffsetY) == 0 + && sleeping == that.sleeping + && Objects.equals(bodyRef, that.bodyRef) + && bodyUuid.equals(that.bodyUuid) + && spaceUuid.equals(that.spaceUuid) + && bodyType == that.bodyType; + } + + @Override + public int hashCode() { + int result = Objects.hash(bodyRef, bodyUuid, spaceUuid, bodyType, sleeping); + result = 31 * result + Float.hashCode(positionX); + result = 31 * result + Float.hashCode(positionY); + result = 31 * result + Float.hashCode(positionZ); + result = 31 * result + Float.hashCode(rotationX); + result = 31 * result + Float.hashCode(rotationY); + result = 31 * result + Float.hashCode(rotationZ); + result = 31 * result + Float.hashCode(rotationW); + result = 31 * result + Float.hashCode(linearVelocityX); + result = 31 * result + Float.hashCode(linearVelocityY); + result = 31 * result + Float.hashCode(linearVelocityZ); + result = 31 * result + Float.hashCode(angularVelocityX); + result = 31 * result + Float.hashCode(angularVelocityY); + result = 31 * result + Float.hashCode(angularVelocityZ); + result = 31 * result + Float.hashCode(centerOfMassOffsetY); + return result; + } + + @Nonnull + @Override + public String toString() { + return "PhysicsBodySnapshot[bodyRef=" + bodyRef + + ", bodyUuid=" + bodyUuid + + ", spaceUuid=" + spaceUuid + + ", bodyType=" + bodyType + + ", position=(" + positionX + ", " + positionY + ", " + positionZ + ')' + + ", rotation=(" + rotationX + ", " + rotationY + ", " + rotationZ + ", " + + rotationW + ')' + + ", linearVelocity=(" + linearVelocityX + ", " + linearVelocityY + ", " + + linearVelocityZ + ')' + + ", angularVelocity=(" + angularVelocityX + ", " + angularVelocityY + ", " + + angularVelocityZ + ')' + + ", centerOfMassOffsetY=" + centerOfMassOffsetY + + ", sleeping=" + sleeping + + ']'; + } +} diff --git a/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshots/PhysicsSnapshotFrame.java b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshots/PhysicsSnapshotFrame.java new file mode 100644 index 00000000..2fe88593 --- /dev/null +++ b/impulse-core/src/main/java/dev/hytalemodding/impulse/core/plugin/snapshots/PhysicsSnapshotFrame.java @@ -0,0 +1,20 @@ +package dev.hytalemodding.impulse.core.plugin.snapshots; + +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; + +/** + * Immutable copied snapshot frame published by PhysicsStore after a completed backend step. + */ +public record PhysicsSnapshotFrame(long sequence, + float dt, + @Nonnull List bodies) { + + public static final PhysicsSnapshotFrame EMPTY = + new PhysicsSnapshotFrame(0L, 0.0f, List.of()); + + public PhysicsSnapshotFrame { + bodies = List.copyOf(Objects.requireNonNull(bodies, "bodies")); + } +} diff --git a/impulse-core/src/module-info/module-info.java b/impulse-core/src/module-info/module-info.java index 71a0fb41..f66ffa73 100644 --- a/impulse-core/src/module-info/module-info.java +++ b/impulse-core/src/module-info/module-info.java @@ -2,21 +2,19 @@ requires transitive impulse.api; requires transitive org.joml; requires static jsr305; - requires static crucible; - exports dev.hytalemodding.impulse.core.plugin.body; exports dev.hytalemodding.impulse.core.plugin.codec; exports dev.hytalemodding.impulse.core.plugin.components; exports dev.hytalemodding.impulse.core.plugin.events; - exports dev.hytalemodding.impulse.core.plugin.joint; - exports dev.hytalemodding.impulse.core.plugin.modules.control; - exports dev.hytalemodding.impulse.core.plugin.modules.worldcollision; + exports dev.hytalemodding.impulse.core.plugin.modules.physicsentity; + exports dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components; + exports dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings; + exports dev.hytalemodding.impulse.core.plugin.modules.physicschunk; + exports dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components; + exports dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings; exports dev.hytalemodding.impulse.core.plugin.persistence; - exports dev.hytalemodding.impulse.core.plugin.resources; + exports dev.hytalemodding.impulse.core.plugin.physics; exports dev.hytalemodding.impulse.core.plugin.settings; - exports dev.hytalemodding.impulse.core.plugin.simulation; - exports dev.hytalemodding.impulse.core.plugin.simulation.query; - exports dev.hytalemodding.impulse.core.plugin.simulation.recorder; - exports dev.hytalemodding.impulse.core.plugin.simulation.view; exports dev.hytalemodding.impulse.core.plugin.snapshot; + exports dev.hytalemodding.impulse.core.plugin.snapshots; } diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/ImpulsePluginBackendSelectionTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/ImpulseBackendRegistryPluginBackendSelectionTest.java similarity index 63% rename from impulse-core/src/test/java/dev/hytalemodding/impulse/core/ImpulsePluginBackendSelectionTest.java rename to impulse-core/src/test/java/dev/hytalemodding/impulse/core/ImpulseBackendRegistryPluginBackendSelectionTest.java index 0dfb9c3b..c1dbc410 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/ImpulsePluginBackendSelectionTest.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/ImpulseBackendRegistryPluginBackendSelectionTest.java @@ -1,13 +1,11 @@ package dev.hytalemodding.impulse.core; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.PhysicsBackend; -import dev.hytalemodding.impulse.api.PhysicsSpace; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; import java.io.IOException; import java.net.URL; @@ -24,48 +22,17 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -class ImpulsePluginBackendSelectionTest { +class ImpulseBackendRegistryPluginBackendSelectionTest { - private static final String BACKEND_SERVICE = - "META-INF/services/dev.hytalemodding.impulse.api.PhysicsBackend"; + private static final String RUNTIME_PROVIDER_SERVICE = + "META-INF/services/dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider"; private static final String SERVICE_PROVIDER_CLASS = - "dev.hytalemodding.impulse.core.testbackend.JarOnlyServiceLoadedBackend"; + "dev.hytalemodding.impulse.core.testbackend.JarOnlyServiceLoadedRuntimeProvider"; private static final BackendId SERVICE_BACKEND_ID = new BackendId("test:service-loaded"); @TempDir private Path tempDir; - @Test - void ownerPoolSizeConfigReportsDefaultAcceptedAndFallbackValues() { - String property = ImpulsePlugin.OWNER_POOL_SIZE_PROPERTY; - String previous = System.getProperty(property); - try { - System.clearProperty(property); - ImpulsePlugin.ConfiguredPositiveInt defaultValue = - ImpulsePlugin.configuredPositiveIntDetails(property, 1); - assertEquals(1, defaultValue.value()); - assertFalse(defaultValue.usedFallback()); - - System.setProperty(property, "2"); - ImpulsePlugin.ConfiguredPositiveInt configuredValue = - ImpulsePlugin.configuredPositiveIntDetails(property, 1); - assertEquals(2, configuredValue.value()); - assertFalse(configuredValue.usedFallback()); - - System.setProperty(property, "0"); - ImpulsePlugin.ConfiguredPositiveInt invalidValue = - ImpulsePlugin.configuredPositiveIntDetails(property, 1); - assertEquals(1, invalidValue.value()); - assertTrue(invalidValue.usedFallback()); - } finally { - if (previous == null) { - System.clearProperty(property); - } else { - System.setProperty(property, previous); - } - } - } - @Test void singleRuntimeProviderIsDefaultBackend() { BackendId backendId = new BackendId("impulse:rapier"); @@ -82,10 +49,11 @@ void discoversClasspathVisibleServiceProviders() throws IOException { try (URLClassLoader loader = new URLClassLoader( new URL[]{providerJar.toUri().toURL()}, Thread.currentThread().getContextClassLoader())) { - List backends = BackendDiscovery.discover(List.of(), loader); + List backends = + BackendDiscovery.discoverRuntimeProviders(List.of(), loader); assertEquals(List.of(SERVICE_BACKEND_ID), backends.stream() - .map(PhysicsBackend::getId) + .map(PhysicsBackendRuntimeProvider::getId) .toList()); } } @@ -96,19 +64,19 @@ void discoversServiceProvidersFromNestedModsJars() throws IOException { Files.createDirectories(backendDirectory); writeServiceJar(backendDirectory.resolve("provider.jar")); - List backends = BackendDiscovery.discover( + List backends = BackendDiscovery.discoverRuntimeProviders( List.of(tempDir.resolve("mods")), Thread.currentThread().getContextClassLoader()); assertEquals(List.of(SERVICE_BACKEND_ID), backends.stream() - .map(PhysicsBackend::getId) + .map(PhysicsBackendRuntimeProvider::getId) .toList()); } private static void writeServiceJar(@Nonnull Path jarPath) throws IOException { Path classFile = compileServiceProviderClass(jarPath.getParent()); try (JarOutputStream jar = new JarOutputStream(Files.newOutputStream(jarPath))) { - jar.putNextEntry(new JarEntry(BACKEND_SERVICE)); + jar.putNextEntry(new JarEntry(RUNTIME_PROVIDER_SERVICE)); jar.write((SERVICE_PROVIDER_CLASS + "\n") .getBytes(StandardCharsets.UTF_8)); jar.closeEntry(); @@ -130,29 +98,32 @@ private static Path compileServiceProviderClass(@Nonnull Path outputRoot) throws package dev.hytalemodding.impulse.core.testbackend; import dev.hytalemodding.impulse.api.BackendId; - import dev.hytalemodding.impulse.api.PhysicsBackend; - import dev.hytalemodding.impulse.api.PhysicsSpace; + import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; + import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; + import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; import javax.annotation.Nonnull; - public final class JarOnlyServiceLoadedBackend implements PhysicsBackend { + public final class JarOnlyServiceLoadedRuntimeProvider + implements PhysicsBackendRuntimeProvider { - public JarOnlyServiceLoadedBackend() { + private final FakePhysicsBackendRuntimeProvider delegate = + new FakePhysicsBackendRuntimeProvider(new BackendId("test:service-loaded"), + false, + false); + + public JarOnlyServiceLoadedRuntimeProvider() { } @Nonnull @Override public BackendId getId() { - return new BackendId("test:service-loaded"); - } - - @Override - public void init() { + return delegate.getId(); } @Nonnull @Override - public PhysicsSpace createSpace() { - throw new UnsupportedOperationException("not used"); + public PhysicsBackendRuntime createRuntime() { + return delegate.createRuntime(); } } """); diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/benchmark/ComponentGranularityStoreProbeTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/benchmark/ComponentGranularityStoreProbeTest.java new file mode 100644 index 00000000..fec06153 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/benchmark/ComponentGranularityStoreProbeTest.java @@ -0,0 +1,797 @@ +package dev.hytalemodding.impulse.core.internal.benchmark; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.query.Query; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.IntFunction; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +class ComponentGranularityStoreProbeTest { + + private static final int DEFAULT_SPACE_COUNT = 1024; + private static final int DEFAULT_BODY_COUNT = 10_000; + private static final int OPTIONAL_STRIDE = 10; + + @Test + void reportsGroupedDomainAndTinyComponentCosts() throws IOException { + int spaceCount = intProperty("impulse.componentProbe.spaceCount", DEFAULT_SPACE_COUNT); + int bodyCount = intProperty("impulse.componentProbe.bodyCount", DEFAULT_BODY_COUNT); + List results = new ArrayList<>(); + + results.add(run("space-grouped-settings", spaceCount, ComponentLayout.GROUPED, false)); + results.add(run("space-split-domain-settings", spaceCount, ComponentLayout.DOMAIN_SPLIT, false)); + results.add(run("space-tiny-settings", spaceCount, ComponentLayout.TINY, false)); + results.add(run("space-split-domain-settings-10pct-optional", + spaceCount, + ComponentLayout.DOMAIN_SPLIT, + true)); + results.add(run("body-grouped-authoring", bodyCount, ComponentLayout.GROUPED, false)); + results.add(run("body-current-domain-authoring", bodyCount, ComponentLayout.DOMAIN_SPLIT, false)); + results.add(run("body-tiny-authoring", bodyCount, ComponentLayout.TINY, false)); + results.add(run("body-current-domain-authoring-10pct-optional", + bodyCount, + ComponentLayout.DOMAIN_SPLIT, + true)); + + Path report = Path.of("build", + "reports", + "impulse", + "component-granularity-probe.tsv"); + Files.createDirectories(report.getParent()); + Files.writeString(report, ScenarioResult.tsv(results)); + + assertEquals(8, results.size()); + for (ScenarioResult result : results) { + assertEquals(result.entities(), result.storeEntityCount(), result.name()); + assertTrue(result.archetypeChunkCount() > 0, result.name()); + assertNotEquals(0L, result.iterationChecksum(), result.name()); + } + } + + @Nonnull + private static ScenarioResult run(@Nonnull String name, + int entities, + @Nonnull ComponentLayout layout, + boolean optionalFragmentation) { + ComponentRegistry registry = new ComponentRegistry<>(); + List> required = layout.registerRequired(registry); + ComponentSpec optional = + optionalFragmentation ? optional(registry) : null; + Store store = registry.addStore(new BenchmarkWorld(name), + EmptyResourceStorage.get()); + try { + forceGc(); + long heapBefore = usedHeap(); + long addStart = System.nanoTime(); + Ref[] refs = addEntities(store, + registry, + required, + optional, + entities); + long addNanos = System.nanoTime() - addStart; + forceGc(); + long heapAfterAdd = usedHeap(); + + long iterateStart = System.nanoTime(); + long checksum = iterate(store, required); + long iterateNanos = System.nanoTime() - iterateStart; + + long targetedIterateStart = System.nanoTime(); + long targetedChecksum = iterate(store, List.of(required.getFirst())); + long targetedIterateNanos = System.nanoTime() - targetedIterateStart; + + long mutateStart = System.nanoTime(); + replaceComponent(store, required.getFirst(), refs); + long mutateNanos = System.nanoTime() - mutateStart; + + int entityCount = store.getEntityCount(); + int archetypeChunkCount = store.getArchetypeChunkCount(); + int archetypeDataCount = store.collectArchetypeChunkData().length; + int requiredPayloadInts = required.stream().mapToInt(ComponentSpec::payloadInts).sum(); + long optionalInstances = optional != null + ? ((long) (entities - 1) / OPTIONAL_STRIDE) + 1L + : 0L; + long componentInstances = (long) entities * required.size() + optionalInstances; + long payloadBytes = ((long) entities * requiredPayloadInts + + optionalInstances * (optional != null ? optional.payloadInts() : 0)) + * Integer.BYTES; + + return new ScenarioResult(name, + entities, + entityCount, + required.size(), + optionalFragmentation ? 1 : 0, + requiredPayloadInts, + componentInstances, + payloadBytes, + Math.max(0L, heapAfterAdd - heapBefore), + addNanos, + iterateNanos, + targetedIterateNanos, + mutateNanos, + archetypeChunkCount, + archetypeDataCount, + checksum, + targetedChecksum); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Nonnull + private static Ref[] addEntities(@Nonnull Store store, + @Nonnull ComponentRegistry registry, + @Nonnull List> required, + ComponentSpec optional, + int entities) { + @SuppressWarnings("unchecked") + Holder[] holders = new Holder[entities]; + for (int entity = 0; entity < entities; entity++) { + Holder holder = registry.newHolder(); + for (ComponentSpec spec : required) { + addComponent(holder, spec, entity); + } + if (optional != null && entity % OPTIONAL_STRIDE == 0) { + addComponent(holder, optional, entity); + } + holders[entity] = holder; + } + return store.addEntities(holders, AddReason.SPAWN); + } + + private static void addComponent( + @Nonnull Holder holder, + @Nonnull ComponentSpec spec, + int seed) { + holder.addComponent(spec.type(), spec.factory().apply(seed)); + } + + private static long iterate(@Nonnull Store store, + @Nonnull List> required) { + Query query = query(required); + long[] checksum = {0L}; + BiConsumer, CommandBuffer> consumer = + (chunk, _) -> checksum[0] += checksum(required, chunk); + store.forEachChunk(query, consumer); + return checksum[0]; + } + + private static long checksum(@Nonnull List> specs, + @Nonnull ArchetypeChunk chunk) { + long checksum = 0L; + for (int index = 0; index < chunk.size(); index++) { + for (ComponentSpec spec : specs) { + checksum += component(chunk, index, spec).checksum(); + } + } + return checksum; + } + + private static T component( + @Nonnull ArchetypeChunk chunk, + int index, + @Nonnull ComponentSpec spec) { + return chunk.getComponent(index, spec.type()); + } + + private static void replaceComponent( + @Nonnull Store store, + @Nonnull ComponentSpec spec, + @Nonnull Ref[] refs) { + for (int index = 0; index < refs.length; index++) { + store.putComponent(refs[index], spec.type(), spec.factory().apply(index + 1)); + } + } + + @Nonnull + @SuppressWarnings("unchecked") + private static Query query( + @Nonnull List> specs) { + Query[] queries = specs.stream() + .map(ComponentSpec::type) + .toArray(Query[]::new); + return Query.and(queries); + } + + @Nonnull + private static ComponentSpec optional( + @Nonnull ComponentRegistry registry) { + return spec(registry, OptionalComponent.class, OptionalComponent::new, 1); + } + + @Nonnull + private static ComponentSpec spec( + @Nonnull ComponentRegistry registry, + @Nonnull Class typeClass, + @Nonnull IntFunction factory, + int payloadInts) { + return new ComponentSpec<>(registry.registerComponent(typeClass, + () -> factory.apply(0)), factory, payloadInts); + } + + private static int intProperty(@Nonnull String name, int defaultValue) { + Integer propertyValue = Integer.getInteger(name); + if (propertyValue != null) { + return propertyValue; + } + String environmentName = name.replaceAll("([a-z0-9])([A-Z])", "$1_$2") + .toUpperCase() + .replace('.', '_') + .replace('-', '_'); + String environmentValue = System.getenv(environmentName); + if (environmentValue == null || environmentValue.isBlank()) { + return defaultValue; + } + return Integer.parseInt(environmentValue); + } + + private static long usedHeap() { + Runtime runtime = Runtime.getRuntime(); + return runtime.totalMemory() - runtime.freeMemory(); + } + + private static void forceGc() { + for (int i = 0; i < 3; i++) { + System.gc(); + try { + Thread.sleep(10L); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + return; + } + } + } + + private enum ComponentLayout { + GROUPED { + @Nonnull + @Override + List> registerRequired( + @Nonnull ComponentRegistry registry) { + return List.of(spec(registry, + GroupedComponent.class, + GroupedComponent::new, + 35)); + } + }, + DOMAIN_SPLIT { + @Nonnull + @Override + List> registerRequired( + @Nonnull ComponentRegistry registry) { + return List.of( + spec(registry, DomainComponentA.class, DomainComponentA::new, 5), + spec(registry, DomainComponentB.class, DomainComponentB::new, 5), + spec(registry, DomainComponentC.class, DomainComponentC::new, 5), + spec(registry, DomainComponentD.class, DomainComponentD::new, 5), + spec(registry, DomainComponentE.class, DomainComponentE::new, 5), + spec(registry, DomainComponentF.class, DomainComponentF::new, 5), + spec(registry, DomainComponentG.class, DomainComponentG::new, 5)); + } + }, + TINY { + @Nonnull + @Override + List> registerRequired( + @Nonnull ComponentRegistry registry) { + return List.of( + spec(registry, TinyComponent01.class, TinyComponent01::new, 1), + spec(registry, TinyComponent02.class, TinyComponent02::new, 1), + spec(registry, TinyComponent03.class, TinyComponent03::new, 1), + spec(registry, TinyComponent04.class, TinyComponent04::new, 1), + spec(registry, TinyComponent05.class, TinyComponent05::new, 1), + spec(registry, TinyComponent06.class, TinyComponent06::new, 1), + spec(registry, TinyComponent07.class, TinyComponent07::new, 1), + spec(registry, TinyComponent08.class, TinyComponent08::new, 1), + spec(registry, TinyComponent09.class, TinyComponent09::new, 1), + spec(registry, TinyComponent10.class, TinyComponent10::new, 1), + spec(registry, TinyComponent11.class, TinyComponent11::new, 1), + spec(registry, TinyComponent12.class, TinyComponent12::new, 1), + spec(registry, TinyComponent13.class, TinyComponent13::new, 1), + spec(registry, TinyComponent14.class, TinyComponent14::new, 1), + spec(registry, TinyComponent15.class, TinyComponent15::new, 1), + spec(registry, TinyComponent16.class, TinyComponent16::new, 1), + spec(registry, TinyComponent17.class, TinyComponent17::new, 1), + spec(registry, TinyComponent18.class, TinyComponent18::new, 1), + spec(registry, TinyComponent19.class, TinyComponent19::new, 1), + spec(registry, TinyComponent20.class, TinyComponent20::new, 1), + spec(registry, TinyComponent21.class, TinyComponent21::new, 1), + spec(registry, TinyComponent22.class, TinyComponent22::new, 1), + spec(registry, TinyComponent23.class, TinyComponent23::new, 1), + spec(registry, TinyComponent24.class, TinyComponent24::new, 1), + spec(registry, TinyComponent25.class, TinyComponent25::new, 1), + spec(registry, TinyComponent26.class, TinyComponent26::new, 1), + spec(registry, TinyComponent27.class, TinyComponent27::new, 1), + spec(registry, TinyComponent28.class, TinyComponent28::new, 1), + spec(registry, TinyComponent29.class, TinyComponent29::new, 1), + spec(registry, TinyComponent30.class, TinyComponent30::new, 1), + spec(registry, TinyComponent31.class, TinyComponent31::new, 1), + spec(registry, TinyComponent32.class, TinyComponent32::new, 1), + spec(registry, TinyComponent33.class, TinyComponent33::new, 1), + spec(registry, TinyComponent34.class, TinyComponent34::new, 1), + spec(registry, TinyComponent35.class, TinyComponent35::new, 1)); + } + }; + + @Nonnull + abstract List> registerRequired( + @Nonnull ComponentRegistry registry); + } + + private record ComponentSpec( + @Nonnull ComponentType type, + @Nonnull IntFunction factory, + int payloadInts) { + } + + private record ScenarioResult(@Nonnull String name, + int entities, + int storeEntityCount, + int requiredComponents, + int optionalComponents, + int requiredPayloadInts, + long componentInstances, + long payloadBytes, + long heapBytes, + long addNanos, + long iterateNanos, + long targetedIterateNanos, + long mutateNanos, + int archetypeChunkCount, + int archetypeDataCount, + long iterationChecksum, + long targetedIterationChecksum) { + + @Nonnull + static String tsv(@Nonnull List results) { + String header = String.join("\t", + "scenario", + "entities", + "requiredComponents", + "optionalComponents", + "requiredPayloadInts", + "componentInstances", + "payloadBytes", + "payloadBytesPerEntity", + "heapBytes", + "bytesPerEntity", + "measuredOverheadBytes", + "overheadBytesPerEntity", + "bytesPerComponentInstance", + "addNsPerEntity", + "iterateNsPerEntity", + "targetedIterateNsPerEntity", + "mutateNsPerEntity", + "archetypeChunkCount", + "archetypeDataCount", + "iterationChecksum", + "targetedIterationChecksum"); + String rows = results.stream() + .map(ScenarioResult::tsvRow) + .collect(Collectors.joining(System.lineSeparator())); + return header + System.lineSeparator() + rows + System.lineSeparator(); + } + + @Nonnull + private String tsvRow() { + return String.join("\t", + name, + Integer.toString(entities), + Integer.toString(requiredComponents), + Integer.toString(optionalComponents), + Integer.toString(requiredPayloadInts), + Long.toString(componentInstances), + Long.toString(payloadBytes), + Long.toString(payloadBytes / Math.max(1, entities)), + Long.toString(heapBytes), + Long.toString(heapBytes / Math.max(1, entities)), + Long.toString(Math.max(0L, heapBytes - payloadBytes)), + Long.toString(Math.max(0L, heapBytes - payloadBytes) / Math.max(1, entities)), + Long.toString(heapBytes / Math.max(1L, componentInstances)), + Long.toString(addNanos / Math.max(1, entities)), + Long.toString(iterateNanos / Math.max(1, entities)), + Long.toString(targetedIterateNanos / Math.max(1, entities)), + Long.toString(mutateNanos / Math.max(1, entities)), + Integer.toString(archetypeChunkCount), + Integer.toString(archetypeDataCount), + Long.toString(iterationChecksum), + Long.toString(targetedIterationChecksum)); + } + } + + private record BenchmarkWorld(@Nonnull String name) { + } + + private interface ProbeComponent extends Component { + + long checksum(); + } + + private abstract static class BaseComponent implements ProbeComponent { + + protected abstract int seed(); + + @Override + public abstract BaseComponent clone(); + } + + private abstract static class GroupedBase extends BaseComponent { + + private final int value00; + private final int value01; + private final int value02; + private final int value03; + private final int value04; + private final int value05; + private final int value06; + private final int value07; + private final int value08; + private final int value09; + private final int value10; + private final int value11; + private final int value12; + private final int value13; + private final int value14; + private final int value15; + private final int value16; + private final int value17; + private final int value18; + private final int value19; + private final int value20; + private final int value21; + private final int value22; + private final int value23; + private final int value24; + private final int value25; + private final int value26; + private final int value27; + private final int value28; + private final int value29; + private final int value30; + private final int value31; + private final int value32; + private final int value33; + private final int value34; + + private GroupedBase(int seed) { + value00 = seed; + value01 = seed + 1; + value02 = seed + 2; + value03 = seed + 3; + value04 = seed + 4; + value05 = seed + 5; + value06 = seed + 6; + value07 = seed + 7; + value08 = seed + 8; + value09 = seed + 9; + value10 = seed + 10; + value11 = seed + 11; + value12 = seed + 12; + value13 = seed + 13; + value14 = seed + 14; + value15 = seed + 15; + value16 = seed + 16; + value17 = seed + 17; + value18 = seed + 18; + value19 = seed + 19; + value20 = seed + 20; + value21 = seed + 21; + value22 = seed + 22; + value23 = seed + 23; + value24 = seed + 24; + value25 = seed + 25; + value26 = seed + 26; + value27 = seed + 27; + value28 = seed + 28; + value29 = seed + 29; + value30 = seed + 30; + value31 = seed + 31; + value32 = seed + 32; + value33 = seed + 33; + value34 = seed + 34; + } + + @Override + protected int seed() { + return value00; + } + + @Override + public long checksum() { + return (long) value00 + value01 + value02 + value03 + value04 + + value05 + value06 + value07 + value08 + value09 + + value10 + value11 + value12 + value13 + value14 + + value15 + value16 + value17 + value18 + value19 + + value20 + value21 + value22 + value23 + value24 + + value25 + value26 + value27 + value28 + value29 + + value30 + value31 + value32 + value33 + value34; + } + } + + private abstract static class DomainBase extends BaseComponent { + + private final int value0; + private final int value1; + private final int value2; + private final int value3; + private final int value4; + + private DomainBase(int seed) { + value0 = seed; + value1 = seed + 1; + value2 = seed + 2; + value3 = seed + 3; + value4 = seed + 4; + } + + @Override + protected int seed() { + return value0; + } + + @Override + public long checksum() { + return (long) value0 + value1 + value2 + value3 + value4; + } + } + + private abstract static class TinyBase extends BaseComponent { + + private final int value; + + private TinyBase(int seed) { + value = seed; + } + + @Override + protected int seed() { + return value; + } + + @Override + public long checksum() { + return value; + } + } + + private static final class GroupedComponent extends GroupedBase { + private GroupedComponent(int seed) { super(seed); } + @Override public GroupedComponent clone() { return new GroupedComponent(seed()); } + } + + private static final class DomainComponentA extends DomainBase { + private DomainComponentA(int seed) { super(seed); } + @Override public DomainComponentA clone() { return new DomainComponentA(seed()); } + } + + private static final class DomainComponentB extends DomainBase { + private DomainComponentB(int seed) { super(seed); } + @Override public DomainComponentB clone() { return new DomainComponentB(seed()); } + } + + private static final class DomainComponentC extends DomainBase { + private DomainComponentC(int seed) { super(seed); } + @Override public DomainComponentC clone() { return new DomainComponentC(seed()); } + } + + private static final class DomainComponentD extends DomainBase { + private DomainComponentD(int seed) { super(seed); } + @Override public DomainComponentD clone() { return new DomainComponentD(seed()); } + } + + private static final class DomainComponentE extends DomainBase { + private DomainComponentE(int seed) { super(seed); } + @Override public DomainComponentE clone() { return new DomainComponentE(seed()); } + } + + private static final class DomainComponentF extends DomainBase { + private DomainComponentF(int seed) { super(seed); } + @Override public DomainComponentF clone() { return new DomainComponentF(seed()); } + } + + private static final class DomainComponentG extends DomainBase { + private DomainComponentG(int seed) { super(seed); } + @Override public DomainComponentG clone() { return new DomainComponentG(seed()); } + } + + private static final class OptionalComponent extends TinyBase { + private OptionalComponent(int seed) { super(seed); } + @Override public OptionalComponent clone() { return new OptionalComponent(seed()); } + } + + private static final class TinyComponent01 extends TinyBase { + private TinyComponent01(int seed) { super(seed); } + @Override public TinyComponent01 clone() { return new TinyComponent01(seed()); } + } + + private static final class TinyComponent02 extends TinyBase { + private TinyComponent02(int seed) { super(seed); } + @Override public TinyComponent02 clone() { return new TinyComponent02(seed()); } + } + + private static final class TinyComponent03 extends TinyBase { + private TinyComponent03(int seed) { super(seed); } + @Override public TinyComponent03 clone() { return new TinyComponent03(seed()); } + } + + private static final class TinyComponent04 extends TinyBase { + private TinyComponent04(int seed) { super(seed); } + @Override public TinyComponent04 clone() { return new TinyComponent04(seed()); } + } + + private static final class TinyComponent05 extends TinyBase { + private TinyComponent05(int seed) { super(seed); } + @Override public TinyComponent05 clone() { return new TinyComponent05(seed()); } + } + + private static final class TinyComponent06 extends TinyBase { + private TinyComponent06(int seed) { super(seed); } + @Override public TinyComponent06 clone() { return new TinyComponent06(seed()); } + } + + private static final class TinyComponent07 extends TinyBase { + private TinyComponent07(int seed) { super(seed); } + @Override public TinyComponent07 clone() { return new TinyComponent07(seed()); } + } + + private static final class TinyComponent08 extends TinyBase { + private TinyComponent08(int seed) { super(seed); } + @Override public TinyComponent08 clone() { return new TinyComponent08(seed()); } + } + + private static final class TinyComponent09 extends TinyBase { + private TinyComponent09(int seed) { super(seed); } + @Override public TinyComponent09 clone() { return new TinyComponent09(seed()); } + } + + private static final class TinyComponent10 extends TinyBase { + private TinyComponent10(int seed) { super(seed); } + @Override public TinyComponent10 clone() { return new TinyComponent10(seed()); } + } + + private static final class TinyComponent11 extends TinyBase { + private TinyComponent11(int seed) { super(seed); } + @Override public TinyComponent11 clone() { return new TinyComponent11(seed()); } + } + + private static final class TinyComponent12 extends TinyBase { + private TinyComponent12(int seed) { super(seed); } + @Override public TinyComponent12 clone() { return new TinyComponent12(seed()); } + } + + private static final class TinyComponent13 extends TinyBase { + private TinyComponent13(int seed) { super(seed); } + @Override public TinyComponent13 clone() { return new TinyComponent13(seed()); } + } + + private static final class TinyComponent14 extends TinyBase { + private TinyComponent14(int seed) { super(seed); } + @Override public TinyComponent14 clone() { return new TinyComponent14(seed()); } + } + + private static final class TinyComponent15 extends TinyBase { + private TinyComponent15(int seed) { super(seed); } + @Override public TinyComponent15 clone() { return new TinyComponent15(seed()); } + } + + private static final class TinyComponent16 extends TinyBase { + private TinyComponent16(int seed) { super(seed); } + @Override public TinyComponent16 clone() { return new TinyComponent16(seed()); } + } + + private static final class TinyComponent17 extends TinyBase { + private TinyComponent17(int seed) { super(seed); } + @Override public TinyComponent17 clone() { return new TinyComponent17(seed()); } + } + + private static final class TinyComponent18 extends TinyBase { + private TinyComponent18(int seed) { super(seed); } + @Override public TinyComponent18 clone() { return new TinyComponent18(seed()); } + } + + private static final class TinyComponent19 extends TinyBase { + private TinyComponent19(int seed) { super(seed); } + @Override public TinyComponent19 clone() { return new TinyComponent19(seed()); } + } + + private static final class TinyComponent20 extends TinyBase { + private TinyComponent20(int seed) { super(seed); } + @Override public TinyComponent20 clone() { return new TinyComponent20(seed()); } + } + + private static final class TinyComponent21 extends TinyBase { + private TinyComponent21(int seed) { super(seed); } + @Override public TinyComponent21 clone() { return new TinyComponent21(seed()); } + } + + private static final class TinyComponent22 extends TinyBase { + private TinyComponent22(int seed) { super(seed); } + @Override public TinyComponent22 clone() { return new TinyComponent22(seed()); } + } + + private static final class TinyComponent23 extends TinyBase { + private TinyComponent23(int seed) { super(seed); } + @Override public TinyComponent23 clone() { return new TinyComponent23(seed()); } + } + + private static final class TinyComponent24 extends TinyBase { + private TinyComponent24(int seed) { super(seed); } + @Override public TinyComponent24 clone() { return new TinyComponent24(seed()); } + } + + private static final class TinyComponent25 extends TinyBase { + private TinyComponent25(int seed) { super(seed); } + @Override public TinyComponent25 clone() { return new TinyComponent25(seed()); } + } + + private static final class TinyComponent26 extends TinyBase { + private TinyComponent26(int seed) { super(seed); } + @Override public TinyComponent26 clone() { return new TinyComponent26(seed()); } + } + + private static final class TinyComponent27 extends TinyBase { + private TinyComponent27(int seed) { super(seed); } + @Override public TinyComponent27 clone() { return new TinyComponent27(seed()); } + } + + private static final class TinyComponent28 extends TinyBase { + private TinyComponent28(int seed) { super(seed); } + @Override public TinyComponent28 clone() { return new TinyComponent28(seed()); } + } + + private static final class TinyComponent29 extends TinyBase { + private TinyComponent29(int seed) { super(seed); } + @Override public TinyComponent29 clone() { return new TinyComponent29(seed()); } + } + + private static final class TinyComponent30 extends TinyBase { + private TinyComponent30(int seed) { super(seed); } + @Override public TinyComponent30 clone() { return new TinyComponent30(seed()); } + } + + private static final class TinyComponent31 extends TinyBase { + private TinyComponent31(int seed) { super(seed); } + @Override public TinyComponent31 clone() { return new TinyComponent31(seed()); } + } + + private static final class TinyComponent32 extends TinyBase { + private TinyComponent32(int seed) { super(seed); } + @Override public TinyComponent32 clone() { return new TinyComponent32(seed()); } + } + + private static final class TinyComponent33 extends TinyBase { + private TinyComponent33(int seed) { super(seed); } + @Override public TinyComponent33 clone() { return new TinyComponent33(seed()); } + } + + private static final class TinyComponent34 extends TinyBase { + private TinyComponent34(int seed) { super(seed); } + @Override public TinyComponent34 clone() { return new TinyComponent34(seed()); } + } + + private static final class TinyComponent35 extends TinyBase { + private TinyComponent35(int seed) { super(seed); } + @Override public TinyComponent35 clone() { return new TinyComponent35(seed()); } + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/benchmark/IdentityFootprintProbeTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/benchmark/IdentityFootprintProbeTest.java new file mode 100644 index 00000000..89eaec2e --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/benchmark/IdentityFootprintProbeTest.java @@ -0,0 +1,461 @@ +package dev.hytalemodding.impulse.core.internal.benchmark; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +class IdentityFootprintProbeTest { + + private static final int DEFAULT_ENTITY_COUNT = 10_000; + private static final int DEFAULT_LOOKUP_PASSES = 5; + private static final long GENERATION = 1L; + private static final long SLOT_MASK = 0x0000_FFFF_FFFF_FFFFL; + + @Test + void reportsUuidAndGenerationalIdentityCosts() throws IOException { + int entityCount = intProperty("impulse.identityProbe.entityCount", + DEFAULT_ENTITY_COUNT); + int lookupPasses = intProperty("impulse.identityProbe.lookupPasses", + DEFAULT_LOOKUP_PASSES); + + List results = List.of( + runUuid("uuid-component-only", entityCount, lookupPasses, false), + runGenerationalId("generational-id-component-only", + entityCount, + lookupPasses, + false), + runUuid("uuid-component-object-index", entityCount, lookupPasses, true), + runGenerationalId("generational-id-component-primitive-index", + entityCount, + lookupPasses, + true)); + + Path report = Path.of("build", + "reports", + "impulse", + "identity-footprint-probe.tsv"); + Files.createDirectories(report.getParent()); + Files.writeString(report, ScenarioResult.tsv(results)); + + assertEquals(4, results.size()); + for (ScenarioResult result : results) { + assertEquals(result.entities(), result.storeEntityCount(), result.name()); + assertTrue(result.archetypeChunkCount() > 0, result.name()); + assertNotEquals(0L, result.scanChecksum(), result.name()); + if (!"none".equals(result.indexKind())) { + assertNotEquals(0L, result.lookupChecksum(), result.name()); + } + } + } + + @Nonnull + private static ScenarioResult runUuid(@Nonnull String name, + int entities, + int lookupPasses, + boolean indexed) { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentType type = + registry.registerComponent(UuidIdentityComponent.class, + () -> new UuidIdentityComponent(uuidFor(0))); + Store store = registry.addStore(new BenchmarkWorld(name), + EmptyResourceStorage.get()); + try { + forceGc(); + long heapBefore = usedHeap(); + long buildStart = System.nanoTime(); + Ref[] refs = addUuidRows(store, registry, type, entities); + Object2ObjectOpenHashMap> index = + indexed ? uuidIndex(refs) : null; + long buildNanos = System.nanoTime() - buildStart; + forceGc(); + long heapAfterBuild = usedHeap(); + + ScanResult scan = scanUuid(store, type); + LookupResult lookup = index != null + ? lookupUuid(index, entities, lookupPasses) + : LookupResult.EMPTY; + + return result(name, + entities, + store, + "uuid", + indexed ? "Object2ObjectOpenHashMap" : "none", + heapAfterBuild - heapBefore, + buildNanos, + scan, + lookup); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Nonnull + private static ScenarioResult runGenerationalId(@Nonnull String name, + int entities, + int lookupPasses, + boolean indexed) { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentType type = + registry.registerComponent(GenerationalIdComponent.class, + () -> new GenerationalIdComponent(generationalId(0))); + Store store = registry.addStore(new BenchmarkWorld(name), + EmptyResourceStorage.get()); + try { + forceGc(); + long heapBefore = usedHeap(); + long buildStart = System.nanoTime(); + Ref[] refs = addGenerationalRows(store, registry, type, entities); + Long2ObjectOpenHashMap> index = + indexed ? generationalIndex(refs) : null; + long buildNanos = System.nanoTime() - buildStart; + forceGc(); + long heapAfterBuild = usedHeap(); + + ScanResult scan = scanGenerationalId(store, type); + LookupResult lookup = index != null + ? lookupGenerationalId(index, entities, lookupPasses) + : LookupResult.EMPTY; + + return result(name, + entities, + store, + "generational-long", + indexed ? "Long2ObjectOpenHashMap" : "none", + heapAfterBuild - heapBefore, + buildNanos, + scan, + lookup); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Nonnull + private static ScenarioResult result(@Nonnull String name, + int entities, + @Nonnull Store store, + @Nonnull String componentKind, + @Nonnull String indexKind, + long heapBytes, + long buildNanos, + @Nonnull ScanResult scan, + @Nonnull LookupResult lookup) { + return new ScenarioResult(name, + entities, + store.getEntityCount(), + componentKind, + indexKind, + Math.max(0L, heapBytes), + buildNanos, + scan.nanos(), + lookup.nanos(), + lookup.count(), + store.getArchetypeChunkCount(), + store.collectArchetypeChunkData().length, + scan.checksum(), + lookup.checksum()); + } + + @Nonnull + private static Ref[] addUuidRows(@Nonnull Store store, + @Nonnull ComponentRegistry registry, + @Nonnull ComponentType type, + int entities) { + @SuppressWarnings("unchecked") + Holder[] holders = new Holder[entities]; + for (int entity = 0; entity < entities; entity++) { + Holder holder = registry.newHolder(); + holder.addComponent(type, new UuidIdentityComponent(uuidFor(entity))); + holders[entity] = holder; + } + return store.addEntities(holders, AddReason.SPAWN); + } + + @Nonnull + private static Ref[] addGenerationalRows( + @Nonnull Store store, + @Nonnull ComponentRegistry registry, + @Nonnull ComponentType type, + int entities) { + @SuppressWarnings("unchecked") + Holder[] holders = new Holder[entities]; + for (int entity = 0; entity < entities; entity++) { + Holder holder = registry.newHolder(); + holder.addComponent(type, new GenerationalIdComponent(generationalId(entity))); + holders[entity] = holder; + } + return store.addEntities(holders, AddReason.SPAWN); + } + + @Nonnull + private static Object2ObjectOpenHashMap> uuidIndex( + @Nonnull Ref[] refs) { + Object2ObjectOpenHashMap> index = + new Object2ObjectOpenHashMap<>(refs.length); + for (int entity = 0; entity < refs.length; entity++) { + index.put(uuidFor(entity), refs[entity]); + } + return index; + } + + @Nonnull + private static Long2ObjectOpenHashMap> generationalIndex( + @Nonnull Ref[] refs) { + Long2ObjectOpenHashMap> index = + new Long2ObjectOpenHashMap<>(refs.length); + for (int entity = 0; entity < refs.length; entity++) { + index.put(generationalId(entity), refs[entity]); + } + return index; + } + + @Nonnull + private static ScanResult scanUuid(@Nonnull Store store, + @Nonnull ComponentType type) { + long start = System.nanoTime(); + long[] checksum = {0L}; + BiConsumer, CommandBuffer> consumer = + (chunk, _) -> { + for (int index = 0; index < chunk.size(); index++) { + checksum[0] += chunk.getComponent(index, type).checksum(); + } + }; + store.forEachChunk(type, consumer); + return new ScanResult(System.nanoTime() - start, checksum[0]); + } + + @Nonnull + private static ScanResult scanGenerationalId(@Nonnull Store store, + @Nonnull ComponentType type) { + long start = System.nanoTime(); + long[] checksum = {0L}; + BiConsumer, CommandBuffer> consumer = + (chunk, _) -> { + for (int index = 0; index < chunk.size(); index++) { + checksum[0] += chunk.getComponent(index, type).checksum(); + } + }; + store.forEachChunk(type, consumer); + return new ScanResult(System.nanoTime() - start, checksum[0]); + } + + @Nonnull + private static LookupResult lookupUuid( + @Nonnull Object2ObjectOpenHashMap> index, + int entities, + int passes) { + UUID[] keys = new UUID[entities]; + for (int entity = 0; entity < entities; entity++) { + keys[entity] = uuidFor(entity); + } + long checksum = 0L; + long start = System.nanoTime(); + for (int pass = 0; pass < passes; pass++) { + for (UUID key : keys) { + Ref ref = index.get(key); + if (ref == null) { + throw new AssertionError("Missing UUID index entry: " + key); + } + checksum += ref.getIndex(); + } + } + return new LookupResult(System.nanoTime() - start, (long) entities * passes, checksum); + } + + @Nonnull + private static LookupResult lookupGenerationalId( + @Nonnull Long2ObjectOpenHashMap> index, + int entities, + int passes) { + long[] keys = new long[entities]; + for (int entity = 0; entity < entities; entity++) { + keys[entity] = generationalId(entity); + } + long checksum = 0L; + long start = System.nanoTime(); + for (int pass = 0; pass < passes; pass++) { + for (long key : keys) { + Ref ref = index.get(key); + if (ref == null) { + throw new AssertionError("Missing generational id index entry: " + key); + } + checksum += ref.getIndex(); + } + } + return new LookupResult(System.nanoTime() - start, (long) entities * passes, checksum); + } + + @Nonnull + private static UUID uuidFor(int index) { + return new UUID(0x496d_7075_6c73_6500L, index + 1L); + } + + private static long generationalId(int index) { + return (GENERATION << 48) | (index & SLOT_MASK); + } + + private static int intProperty(@Nonnull String name, int defaultValue) { + Integer propertyValue = Integer.getInteger(name); + if (propertyValue != null) { + return propertyValue; + } + String environmentName = name.replaceAll("([a-z0-9])([A-Z])", "$1_$2") + .toUpperCase() + .replace('.', '_') + .replace('-', '_'); + String environmentValue = System.getenv(environmentName); + if (environmentValue == null || environmentValue.isBlank()) { + return defaultValue; + } + return Integer.parseInt(environmentValue); + } + + private static long usedHeap() { + Runtime runtime = Runtime.getRuntime(); + return runtime.totalMemory() - runtime.freeMemory(); + } + + private static void forceGc() { + for (int i = 0; i < 3; i++) { + System.gc(); + try { + Thread.sleep(10L); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + return; + } + } + } + + private record ScanResult(long nanos, long checksum) { + } + + private record LookupResult(long nanos, long count, long checksum) { + + private static final LookupResult EMPTY = new LookupResult(0L, 0L, 0L); + } + + private record ScenarioResult(@Nonnull String name, + int entities, + int storeEntityCount, + @Nonnull String componentKind, + @Nonnull String indexKind, + long heapBytes, + long buildNanos, + long scanNanos, + long lookupNanos, + long lookupCount, + int archetypeChunkCount, + int archetypeDataCount, + long scanChecksum, + long lookupChecksum) { + + @Nonnull + static String tsv(@Nonnull List results) { + String header = String.join("\t", + "scenario", + "entities", + "componentKind", + "indexKind", + "heapBytes", + "bytesPerEntity", + "buildNsPerEntity", + "scanNsPerEntity", + "lookupNsPerLookup", + "lookupCount", + "archetypeChunkCount", + "archetypeDataCount", + "scanChecksum", + "lookupChecksum"); + String rows = results.stream() + .map(ScenarioResult::tsvRow) + .collect(Collectors.joining(System.lineSeparator())); + return header + System.lineSeparator() + rows + System.lineSeparator(); + } + + @Nonnull + private String tsvRow() { + return String.join("\t", + name, + Integer.toString(entities), + componentKind, + indexKind, + Long.toString(heapBytes), + Long.toString(heapBytes / Math.max(1, entities)), + Long.toString(buildNanos / Math.max(1, entities)), + Long.toString(scanNanos / Math.max(1, entities)), + Long.toString(lookupNanos / Math.max(1L, lookupCount)), + Long.toString(lookupCount), + Integer.toString(archetypeChunkCount), + Integer.toString(archetypeDataCount), + Long.toString(scanChecksum), + Long.toString(lookupChecksum)); + } + } + + private record BenchmarkWorld(@Nonnull String name) { + } + + private static final class UuidIdentityComponent implements Component { + + @Nonnull + private final UUID uuid; + + private UuidIdentityComponent(@Nonnull UUID uuid) { + this.uuid = uuid; + } + + private long checksum() { + return uuid.getMostSignificantBits() ^ uuid.getLeastSignificantBits(); + } + + @Nonnull + @Override + public UuidIdentityComponent clone() { + return new UuidIdentityComponent(uuid); + } + } + + private static final class GenerationalIdComponent implements Component { + + private final long id; + + private GenerationalIdComponent(long id) { + this.id = id; + } + + private long checksum() { + return id; + } + + @Nonnull + @Override + public GenerationalIdComponent clone() { + return new GenerationalIdComponent(id); + } + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/CleanCommandLifecycleGuardTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/CleanCommandLifecycleGuardTest.java new file mode 100644 index 00000000..7759639e --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/CleanCommandLifecycleGuardTest.java @@ -0,0 +1,151 @@ +package dev.hytalemodding.impulse.core.internal.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.thread.TickingThread; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent.PartKind; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsSnapshotFrame; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Vector3d; +import org.junit.jupiter.api.Test; + +class CleanCommandLifecycleGuardTest { + + @Test + void radiusCleanUsesEcsOwnershipInsteadOfLegacyKind() throws Exception { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("clean-radius-kind-filter")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID bodyUuid = UUID.randomUUID(); + UUID generatedUuid = UUID.randomUUID(); + UUID spaceUuid = UUID.randomUUID(); + addBodyIdentityRow(store, bodyUuid, false); + addBodyIdentityRow(store, generatedUuid, true); + publishSnapshots(store, bodyUuid, generatedUuid, spaceUuid); + + Set selected = selectBodyUuidsNear(store, new Vector3d(), 10.0f); + + assertEquals(Set.of(bodyUuid), selected); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + private static void publishSnapshots(@Nonnull Store store, + @Nonnull UUID bodyUuid, + @Nonnull UUID generatedUuid, + @Nonnull UUID spaceUuid) { + store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .putSpace(new SpaceId(1), spaceUuid); + store.getResource(PhysicsSnapshotResource.getResourceType()) + .publish(new PhysicsSnapshotFrame(1L, + 0.05f, + List.of(snapshot(bodyUuid, spaceUuid), snapshot(generatedUuid, spaceUuid)))); + } + + @Nonnull + private static Ref addBodyIdentityRow(@Nonnull Store store, + @Nonnull UUID bodyUuid, + boolean generatedChunkCollisionBody) { + Ref ref = store.addEntity(PhysicsEntities.entityHolder(store, + bodyUuid), + AddReason.SPAWN); + store.getExternalData().putRefForUUID(bodyUuid, + ref); + if (generatedChunkCollisionBody) { + store.putComponent(ref, + ChunkCollisionSourceComponent.getComponentType(), + new ChunkCollisionSourceComponent("test-source", + 0, + 0, + 0, + "test-payload", + PartKind.BOX, + 0)); + } + return ref; + } + + @Nonnull + private static PhysicsBodySnapshot snapshot(@Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid) { + return PhysicsBodySnapshot.of(bodyUuid, + spaceUuid, + PhysicsBodyType.DYNAMIC, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + false); + } + + @Nonnull + private static Set selectBodyUuidsNear(@Nonnull Store store, + @Nonnull Vector3d center, + float radius) throws Exception { + Method selectBodiesNear = CleanCommand.class.getDeclaredMethod("selectBodiesNear", + Store.class, + Vector3d.class, + float.class); + selectBodiesNear.setAccessible(true); + Object selected = selectBodiesNear.invoke(null, store, center, radius); + Method bodyUuids = selected.getClass().getDeclaredMethod("bodyUuids"); + bodyUuids.setAccessible(true); + return (Set) bodyUuids.invoke(selected); + } + + private static void markCurrentThreadAsWorldThread(@Nonnull Store store) { + try { + Method setThread = TickingThread.class.getDeclaredMethod("setThread", Thread.class); + setThread.setAccessible(true); + setThread.invoke(store.getExternalData().getWorld(), Thread.currentThread()); + } catch (NoSuchMethodException | IllegalAccessException exception) { + throw new AssertionError("Could not mark test world thread", exception); + } catch (InvocationTargetException exception) { + throw new AssertionError("Could not mark test world thread", + exception.getTargetException()); + } + } + +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseBackendRegistryCommandTreeRegistryTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseBackendRegistryCommandTreeRegistryTest.java new file mode 100644 index 00000000..82a07f65 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseBackendRegistryCommandTreeRegistryTest.java @@ -0,0 +1,33 @@ +package dev.hytalemodding.impulse.core.internal.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import com.hypixel.hytale.server.core.command.system.AbstractCommand; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class ImpulseBackendRegistryCommandTreeRegistryTest { + + @AfterEach + void resetRegistry() { + ImpulseCommandTreeRegistry.resetForTests(); + } + + @Test + void coreRootDoesNotOwnPhysicsChunkCommandsByDefault() { + ImpulseCommand root = ImpulseCommandTreeRegistry.createRootCommandForTests(); + + assertFalse(root.getSubCommands().containsKey("physicschunk")); + assertFalse(debug(root).getSubCommands().containsKey("physicschunk")); + assertFalse(settings(root).getSubCommands().containsKey("collision-lod")); + assertFalse(settings(root).getSubCommands().containsKey("visual")); + } + + private static AbstractCommand debug(ImpulseCommand root) { + return root.getSubCommands().get("debug"); + } + + private static AbstractCommand settings(ImpulseCommand root) { + return root.getSubCommands().get("settings"); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseCommandContributionRegistryTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseCommandContributionRegistryTest.java deleted file mode 100644 index 5dcd1ab5..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/ImpulseCommandContributionRegistryTest.java +++ /dev/null @@ -1,58 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.commands; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.hypixel.hytale.server.core.command.system.AbstractCommand; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.commands.WorldCollisionCommandContributions; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -class ImpulseCommandContributionRegistryTest { - - @AfterEach - void resetRegistry() { - ImpulseCommandContributionRegistry.resetForTests(); - } - - @Test - void coreRootDoesNotOwnWorldCollisionCommandsByDefault() { - ImpulseCommand root = ImpulseCommandContributionRegistry.createRootCommandForTests(); - - assertFalse(root.getSubCommands().containsKey("worldcollision")); - assertFalse(settings(root).getSubCommands().containsKey("collision-lod")); - } - - @Test - void worldCollisionContributesCommandsUnderImpulseRoot() { - WorldCollisionCommandContributions.register(); - - ImpulseCommand root = ImpulseCommandContributionRegistry.createRootCommandForTests(); - - AbstractCommand worldCollision = root.getSubCommands().get("worldcollision"); - assertTrue(root.getSubCommands().containsKey("worldcollision")); - assertTrue(worldCollision.getSubCommands().containsKey("settings")); - assertTrue(worldCollision.getSubCommands().containsKey("perf")); - assertTrue(settings(root).getSubCommands().containsKey("collision-lod")); - } - - @Test - void worldCollisionContributionsAreIdempotentAndRemovable() { - WorldCollisionCommandContributions.register(); - WorldCollisionCommandContributions.register(); - - ImpulseCommand contributed = ImpulseCommandContributionRegistry.createRootCommandForTests(); - assertTrue(contributed.getSubCommands().containsKey("worldcollision")); - assertTrue(settings(contributed).getSubCommands().containsKey("collision-lod")); - - WorldCollisionCommandContributions.unregister(); - - ImpulseCommand removed = ImpulseCommandContributionRegistry.createRootCommandForTests(); - assertFalse(removed.getSubCommands().containsKey("worldcollision")); - assertFalse(settings(removed).getSubCommands().containsKey("collision-lod")); - } - - private static AbstractCommand settings(ImpulseCommand root) { - return root.getSubCommands().get("settings"); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/PhysicsChunkCommandSetTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/PhysicsChunkCommandSetTest.java new file mode 100644 index 00000000..d7be4b24 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/PhysicsChunkCommandSetTest.java @@ -0,0 +1,57 @@ +package dev.hytalemodding.impulse.core.internal.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.server.core.command.system.AbstractCommand; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.commands.PhysicsChunkCommandSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class PhysicsChunkCommandSetTest { + + @AfterEach + void resetRegistry() { + ImpulseCommandTreeRegistry.resetForTests(); + } + + @Test + void physicsChunkRegistersCommandSetUnderImpulseRoot() { + PhysicsChunkCommandSet.register(); + + ImpulseCommand root = ImpulseCommandTreeRegistry.createRootCommandForTests(); + + AbstractCommand physicsChunk = root.getSubCommands().get("physicschunk"); + assertTrue(root.getSubCommands().containsKey("physicschunk")); + assertTrue(physicsChunk.getSubCommands().containsKey("settings")); + assertTrue(physicsChunk.getSubCommands().containsKey("perf")); + assertTrue(debug(root).getSubCommands().containsKey("physicschunk")); + assertTrue(settings(root).getSubCommands().containsKey("collision-lod")); + } + + @Test + void physicsChunkCommandSetIsIdempotentAndRemovable() { + PhysicsChunkCommandSet.register(); + PhysicsChunkCommandSet.register(); + + ImpulseCommand registered = ImpulseCommandTreeRegistry.createRootCommandForTests(); + assertTrue(registered.getSubCommands().containsKey("physicschunk")); + assertTrue(debug(registered).getSubCommands().containsKey("physicschunk")); + assertTrue(settings(registered).getSubCommands().containsKey("collision-lod")); + + PhysicsChunkCommandSet.unregister(); + + ImpulseCommand removed = ImpulseCommandTreeRegistry.createRootCommandForTests(); + assertFalse(removed.getSubCommands().containsKey("physicschunk")); + assertFalse(debug(removed).getSubCommands().containsKey("physicschunk")); + assertFalse(settings(removed).getSubCommands().containsKey("collision-lod")); + } + + private static AbstractCommand debug(ImpulseCommand root) { + return root.getSubCommands().get("debug"); + } + + private static AbstractCommand settings(ImpulseCommand root) { + return root.getSubCommands().get("settings"); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/PhysicsEntityCommandSetTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/PhysicsEntityCommandSetTest.java new file mode 100644 index 00000000..3d822abc --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/PhysicsEntityCommandSetTest.java @@ -0,0 +1,47 @@ +package dev.hytalemodding.impulse.core.internal.commands; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.server.core.command.system.AbstractCommand; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.commands.PhysicsEntityCommandSet; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class PhysicsEntityCommandSetTest { + + @AfterEach + void resetRegistry() { + ImpulseCommandTreeRegistry.resetForTests(); + } + + @Test + void physicsEntityRegistersVisualSettingsUnderImpulseSettings() { + PhysicsEntityCommandSet.register(); + + ImpulseCommand root = ImpulseCommandTreeRegistry.createRootCommandForTests(); + + AbstractCommand visual = settings(root).getSubCommands().get("visual"); + assertTrue(settings(root).getSubCommands().containsKey("visual")); + assertTrue(visual.getSubCommands().containsKey("sync")); + assertTrue(visual.getSubCommands().containsKey("materialization")); + } + + @Test + void physicsEntityCommandSetIsIdempotentAndRemovable() { + PhysicsEntityCommandSet.register(); + PhysicsEntityCommandSet.register(); + + ImpulseCommand registered = ImpulseCommandTreeRegistry.createRootCommandForTests(); + assertTrue(settings(registered).getSubCommands().containsKey("visual")); + + PhysicsEntityCommandSet.unregister(); + + ImpulseCommand removed = ImpulseCommandTreeRegistry.createRootCommandForTests(); + assertFalse(settings(removed).getSubCommands().containsKey("visual")); + } + + private static AbstractCommand settings(ImpulseCommand root) { + return root.getSubCommands().get("settings"); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/SpaceSelectionTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/SpaceSelectionTest.java deleted file mode 100644 index 5849f29b..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/SpaceSelectionTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.commands; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; - -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.Test; - -class SpaceSelectionTest { - - private static final AtomicInteger BACKEND_COUNTER = new AtomicInteger(); - - @Test - void specifiedSpaceIdReturnsExistingExplicitSpace() { - Fixture fixture = fixture(); - SpaceId spaceId = new SpaceId(12); - fixture.resource().createSpace(fixture.backend().getId(), - spaceId, - "test-world", - PhysicsSpaceSettings.defaults()); - - assertEquals(spaceId, SpaceSelection.specifiedSpaceId(fixture.resource(), 12)); - } - - @Test - void specifiedSpaceIdRejectsInvalidOrMissingSpace() { - Fixture fixture = fixture(); - fixture.resource().createSpace(fixture.backend().getId(), - new SpaceId(7), - "test-world", - PhysicsSpaceSettings.defaults()); - - assertNull(SpaceSelection.specifiedSpaceId(fixture.resource(), 0)); - assertNull(SpaceSelection.specifiedSpaceId(fixture.resource(), -1)); - assertNull(SpaceSelection.specifiedSpaceId(fixture.resource(), 8)); - } - - @Test - void firstRegisteredSpaceIdSelectsLowestStableId() { - Fixture fixture = fixture(); - SpaceId higher = new SpaceId(20); - SpaceId lower = new SpaceId(5); - fixture.resource().createSpace(fixture.backend().getId(), - higher, - "test-world", - PhysicsSpaceSettings.defaults()); - fixture.resource().createSpace(fixture.backend().getId(), - lower, - "test-world", - PhysicsSpaceSettings.defaults()); - - assertEquals(lower, SpaceSelection.firstRegisteredSpaceId(fixture.resource())); - } - - @Test - void firstRegisteredSpaceIdReturnsNullWhenNoSpacesExist() { - Fixture fixture = fixture(); - - assertNull(SpaceSelection.firstRegisteredSpaceId(fixture.resource())); - } - - private static Fixture fixture() { - FakePhysicsBackend backend = new FakePhysicsBackend("test:space-selection-" - + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - return new Fixture(backend, new LegacyLiveHandleTestResource()); - } - - private record Fixture(FakePhysicsBackend backend, LegacyLiveHandleTestResource resource) { - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceCommandDeleteTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceCommandDeleteTest.java new file mode 100644 index 00000000..caeacc51 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceCommandDeleteTest.java @@ -0,0 +1,284 @@ +package dev.hytalemodding.impulse.core.internal.commands.space; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.thread.TickingThread; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkStoreTypes; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent.PartKind; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsSnapshotFrame; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class SpaceCommandDeleteTest { + + private static final BackendId BACKEND_ID = new BackendId("test:space-delete"); + + @Test + void deleteCoreReportsInvalidMissingAndUnboundSpaces() { + StoreFixture fixture = store("space-delete-invalid"); + try { + Store store = fixture.store(); + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(92); + store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .putSpace(new SpaceId(92), spaceUuid); + + assertEquals(SpaceDeleteSupport.DeleteOutcome.INVALID, + deleteOnWorldThread(store, 0).outcome()); + assertEquals(SpaceDeleteSupport.DeleteOutcome.MISSING, + deleteOnWorldThread(store, 93).outcome()); + assertEquals(SpaceDeleteSupport.DeleteOutcome.UNBOUND, + deleteOnWorldThread(store, 92).outcome()); + } finally { + fixture.close(); + } + } + + @Test + void deleteCoreRemovesEmptySpaceOnWorldThread() { + StoreFixture fixture = store("space-delete-core"); + try { + Store store = fixture.store(); + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(91); + Ref spaceRef = addSpace(store, spaceUuid); + bindSpaceId(store, new SpaceId(91), spaceUuid, spaceRef); + + SpaceDeleteSupport.DeleteResult result = deleteOnWorldThread(store, 91); + + assertEquals(SpaceDeleteSupport.DeleteOutcome.DELETED, result.outcome()); + assertEquals(91, result.rawSpaceId()); + assertEquals(0, result.backendBodies()); + assertEquals(0, result.joints()); + assertFalse(spaceRef.isValid()); + } finally { + fixture.close(); + } + } + + @Test + void deleteCoreRejectsRegisteredBodies() { + StoreFixture fixture = store("space-delete-not-empty"); + try { + Store store = fixture.store(); + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(94); + Ref spaceRef = addSpace(store, spaceUuid); + bindSpaceId(store, new SpaceId(94), spaceUuid, spaceRef); + store.getResource(PhysicsSnapshotResource.getResourceType()) + .publish(new PhysicsSnapshotFrame(1L, + 0.05f, + List.of(PhysicsBodySnapshot.of(uuid(95), + spaceUuid, + PhysicsBodyType.DYNAMIC, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + false)))); + + SpaceDeleteSupport.DeleteResult result = deleteOnWorldThread(store, 94); + + assertEquals(SpaceDeleteSupport.DeleteOutcome.NOT_EMPTY, result.outcome()); + assertEquals(1, result.registeredBodies()); + assertEquals(0, result.backendBodies()); + assertEquals(0, result.joints()); + assertTrue(spaceRef.isValid()); + } finally { + fixture.close(); + } + } + + @Test + void deleteCoreRemovesStreamingChunkCollisionBodies() { + StoreFixture fixture = store("space-delete-streaming-chunks"); + try { + Store store = fixture.store(); + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(96); + Ref spaceRef = addSpace(store, spaceUuid); + bindSpaceId(store, new SpaceId(96), spaceUuid, spaceRef); + UUID chunkBodyUuid = uuid(97); + Ref chunkBodyRef = addChunkCollisionBody(store, + spaceUuid, + spaceRef, + chunkBodyUuid); + store.getResource(PhysicsSnapshotResource.getResourceType()) + .publish(new PhysicsSnapshotFrame(1L, + 0.05f, + List.of(snapshot(chunkBodyUuid, spaceUuid)))); + assertEquals(1, PhysicsBodies.registrationCount(store, new SpaceId(96))); + + SpaceDeleteSupport.DeleteResult result = deleteOnWorldThread(store, 96); + + assertEquals(SpaceDeleteSupport.DeleteOutcome.DELETED, result.outcome()); + assertEquals(96, result.rawSpaceId()); + assertFalse(spaceRef.isValid()); + assertFalse(chunkBodyRef.isValid()); + assertEquals(0, PhysicsBodies.registrationCount(store, new SpaceId(96))); + } finally { + fixture.close(); + } + } + + private static void bindSpaceId(@Nonnull Store store, + @Nonnull SpaceId spaceId, + @Nonnull UUID spaceUuid, + @Nonnull Ref spaceRef) { + store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .putSpace(spaceId, spaceUuid); + store.getExternalData().putRefForUUID(spaceUuid, spaceRef); + } + + @Nonnull + private static StoreFixture store(@Nonnull String worldName) { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsChunkStoreTypes.registerPhysicsStoreResourceTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world(worldName)), + EmptyResourceStorage.get()); + return new StoreFixture(registry, store); + } + + @Nonnull + private static SpaceDeleteSupport.DeleteResult deleteOnWorldThread( + @Nonnull Store store, + int rawSpaceId) { + return SpaceDeleteSupport.deleteOnWorldThread(store.getExternalData().getWorld(), + store, + rawSpaceId); + } + + @Nonnull + private static Ref addSpace(@Nonnull Store store, + @Nonnull UUID spaceUuid) { + Ref ref = store.addEntity(PhysicsEntities.spaceHolder(store, + spaceUuid, + new SpaceComponent(BACKEND_ID, new Vector3f(0.0f, -9.81f, 0.0f))), + AddReason.SPAWN); + assertNotNull(ref); + return ref; + } + + @Nonnull + private static Ref addChunkCollisionBody(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull Ref spaceRef, + @Nonnull UUID bodyUuid) { + Ref ref = store.addEntity(PhysicsEntities.entityHolder(store, + bodyUuid), + AddReason.SPAWN); + assertNotNull(ref); + BodyComponent body = new BodyComponent(spaceUuid); + body.setSpaceRef(spaceRef); + store.putComponent(ref, BodyComponent.getComponentType(), body); + store.putComponent(ref, + ChunkCollisionSourceComponent.getComponentType(), + new ChunkCollisionSourceComponent("test-source", + 0, + 0, + 0, + "test-payload", + PartKind.BOX, + 0)); + store.getExternalData().putRefForUUID(bodyUuid, ref); + return ref; + } + + @Nonnull + private static PhysicsBodySnapshot snapshot(@Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid) { + return PhysicsBodySnapshot.of(bodyUuid, + spaceUuid, + PhysicsBodyType.DYNAMIC, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + false); + } + + @Nonnull + private static UUID uuid(long leastSignificantBits) { + return new UUID(0L, leastSignificantBits); + } + + private record StoreFixture(@Nonnull ComponentRegistry registry, + @Nonnull Store store) { + + private void close() { + if (!store.isShutdown()) { + registry.removeStore(store); + } + registry.shutdown(); + PhysicsChunkStoreTypes.clearPhysicsStoreResourceTypes(); + } + } + + private static void markCurrentThreadAsWorldThread(@Nonnull Store store) { + try { + Method setThread = TickingThread.class.getDeclaredMethod("setThread", Thread.class); + setThread.setAccessible(true); + setThread.invoke(store.getExternalData().getWorld(), Thread.currentThread()); + } catch (NoSuchMethodException | IllegalAccessException exception) { + throw new AssertionError("Could not mark test world thread", exception); + } catch (InvocationTargetException exception) { + throw new AssertionError("Could not mark test world thread", + exception.getTargetException()); + } + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceSelectionTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceSelectionTest.java new file mode 100644 index 00000000..492bd628 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/commands/space/SpaceSelectionTest.java @@ -0,0 +1,47 @@ +package dev.hytalemodding.impulse.core.internal.commands.space; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class SpaceSelectionTest { + + @Test + void specifiedSpaceIdReturnsExistingExplicitSpace() { + PhysicsSpaceCompatibilityIndexResource compatibility = new PhysicsSpaceCompatibilityIndexResource(); + SpaceId spaceId = new SpaceId(12); + compatibility.putSpace(spaceId, UUID.randomUUID()); + + assertEquals(spaceId, SpaceSelection.specifiedSpaceId(compatibility, 12)); + } + + @Test + void specifiedSpaceIdRejectsInvalidOrMissingSpace() { + PhysicsSpaceCompatibilityIndexResource compatibility = new PhysicsSpaceCompatibilityIndexResource(); + compatibility.putSpace(new SpaceId(7), UUID.randomUUID()); + + assertNull(SpaceSelection.specifiedSpaceId(compatibility, 0)); + assertNull(SpaceSelection.specifiedSpaceId(compatibility, -1)); + assertNull(SpaceSelection.specifiedSpaceId(compatibility, 8)); + } + + @Test + void firstRegisteredSpaceIdSelectsLowestStableId() { + PhysicsSpaceCompatibilityIndexResource compatibility = new PhysicsSpaceCompatibilityIndexResource(); + SpaceId higher = new SpaceId(20); + SpaceId lower = new SpaceId(5); + compatibility.putSpace(higher, UUID.randomUUID()); + compatibility.putSpace(lower, UUID.randomUUID()); + + assertEquals(lower, SpaceSelection.firstRegisteredSpaceId(compatibility)); + } + + @Test + void firstRegisteredSpaceIdReturnsNullWhenNoSpacesExist() { + assertNull(SpaceSelection.firstRegisteredSpaceId(new PhysicsSpaceCompatibilityIndexResource())); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleBackendsTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleBackendsTest.java deleted file mode 100644 index d0577b3d..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/crucible/CrucibleBackendsTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.crucible; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; -import java.util.List; -import org.junit.jupiter.api.Test; - -class CrucibleBackendsTest { - - @Test - void configuredBackendWinsWhenRegistered() { - BackendId bullet = new BackendId("impulse:bullet"); - BackendId rapier = new BackendId("impulse:rapier"); - - assertEquals(bullet, CrucibleBackends.selectBackendId(List.of( - provider(bullet), - provider(rapier)), "impulse:bullet")); - } - - @Test - void rapierIsPreferredWhenMultipleBackendsAreRegistered() { - BackendId bullet = new BackendId("impulse:bullet"); - BackendId rapier = new BackendId("impulse:rapier"); - - assertEquals(rapier, CrucibleBackends.selectBackendId(List.of( - provider(bullet), - provider(rapier)), null)); - } - - @Test - void singleBackendIsSelectedWhenRapierIsUnavailable() { - BackendId bullet = new BackendId("impulse:bullet"); - - assertEquals(bullet, CrucibleBackends.selectBackendId(List.of( - provider(bullet)), null)); - } - - @Test - void multipleNonRapierBackendsRequireExplicitConfiguration() { - assertThrows(IllegalStateException.class, () -> CrucibleBackends.selectBackendId(List.of( - provider(new BackendId("impulse:alpha")), - provider(new BackendId("impulse:beta"))), null)); - } - - @Test - void configuredBackendMustBeRegistered() { - assertThrows(IllegalStateException.class, () -> CrucibleBackends.selectBackendId(List.of( - provider(new BackendId("impulse:rapier"))), "impulse:missing")); - } - - private static FakePhysicsBackendRuntimeProvider provider(BackendId id) { - return new FakePhysicsBackendRuntimeProvider(id, false, false); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/ImpulseBackendRegistrySubPluginRegistrationTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/ImpulseBackendRegistrySubPluginRegistrationTest.java new file mode 100644 index 00000000..10744a98 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/ImpulseBackendRegistrySubPluginRegistrationTest.java @@ -0,0 +1,158 @@ +package dev.hytalemodding.impulse.core.internal.modules; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.codec.ExtraInfo; +import com.hypixel.hytale.codec.util.RawJsonReader; +import com.hypixel.hytale.common.plugin.PluginIdentifier; +import com.hypixel.hytale.common.plugin.PluginManifest; +import com.hypixel.hytale.common.semver.Semver; +import com.hypixel.hytale.common.semver.SemverRange; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +class ImpulseBackendRegistrySubPluginRegistrationTest { + + @Test + void generatedManifestSubPluginsSupportHytalePendingLoadInheritance() throws IOException { + PluginManifest parent = decodeGeneratedManifest(); + PluginIdentifier parentId = new PluginIdentifier(parent); + + List prepared = ImpulseSubPluginRegistration.prepareSubPluginManifests(parent); + for (PluginManifest subPlugin : prepared) { + assertTrue(subPlugin.getDependencies().containsKey(parentId)); + } + assertSubPluginLoadsBefore(parent, "ImpulsePhysicsEntity", "ImpulseControl"); + assertSubPluginLoadsBefore(parent, "ImpulsePhysicsEntity", "ImpulsePhysicsChunk"); + assertSubPluginMain(parent, + "ImpulseControl", + "dev.hytalemodding.impulse.builtin.control.ImpulseControlPlugin"); + assertSubPluginMain(parent, + "ImpulsePhysicsEntity", + "dev.hytalemodding.impulse.core.internal.modules.physicsentity.PhysicsEntityModule"); + assertSubPluginMain(parent, + "ImpulsePhysicsChunk", + "dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkSubPlugin"); + } + + @Test + void preparesEverySubPluginManifestForDynamicLoad() { + PluginManifest parent = manifest("HytaleModding", + "Impulse", + "dev.hytalemodding.impulse.core.ImpulsePlugin", + List.of( + manifest(null, + "FixtureSubPlugin", + "example.FixtureSubPlugin", + List.of(), + false)), + false); + + List prepared = + ImpulseSubPluginRegistration.prepareSubPluginManifests(parent); + + assertEquals(1, prepared.size()); + assertPreparedSubPlugin(prepared.getFirst(), "FixtureSubPlugin", false); + } + + private static void assertPreparedSubPlugin(PluginManifest manifest, + String expectedName, + boolean expectedDisabledByDefault) { + PluginIdentifier parentId = new PluginIdentifier("HytaleModding", "Impulse"); + + assertEquals("HytaleModding", manifest.getGroup()); + assertEquals(expectedName, manifest.getName()); + assertEquals(expectedDisabledByDefault, manifest.isDisabledByDefault()); + assertTrue(manifest.getDependencies().containsKey(parentId)); + } + + private static void assertSubPluginLoadsBefore(@Nonnull PluginManifest parent, + @Nonnull String subPluginName, + @Nonnull String dependencyName) { + PluginIdentifier dependencyId = new PluginIdentifier("HytaleModding", dependencyName); + for (PluginManifest subPlugin : parent.getSubPlugins()) { + if (subPluginName.equals(subPlugin.getName())) { + assertTrue(subPlugin.getLoadBefore().containsKey(dependencyId), + subPluginName + " should load before " + dependencyName); + return; + } + } + throw new AssertionError("Missing subplugin " + subPluginName); + } + + private static void assertSubPluginDoesNotLoadBefore(@Nonnull PluginManifest parent, + @Nonnull String subPluginName, + @Nonnull String dependencyName) { + PluginIdentifier dependencyId = new PluginIdentifier("HytaleModding", dependencyName); + for (PluginManifest subPlugin : parent.getSubPlugins()) { + if (subPluginName.equals(subPlugin.getName())) { + assertFalse(subPlugin.getLoadBefore().containsKey(dependencyId), + subPluginName + " should not order against " + dependencyName); + return; + } + } + throw new AssertionError("Missing subplugin " + subPluginName); + } + + private static void assertMissingSubPlugin(@Nonnull PluginManifest parent, + @Nonnull String subPluginName) { + for (PluginManifest subPlugin : parent.getSubPlugins()) { + assertFalse(subPluginName.equals(subPlugin.getName()), + "Unexpected bundled subplugin " + subPluginName); + } + } + + private static void assertSubPluginMain(@Nonnull PluginManifest parent, + @Nonnull String subPluginName, + @Nonnull String expectedMain) { + for (PluginManifest subPlugin : parent.getSubPlugins()) { + if (subPluginName.equals(subPlugin.getName())) { + assertEquals(expectedMain, subPlugin.getMain()); + return; + } + } + throw new AssertionError("Missing subplugin " + subPluginName); + } + + private static PluginManifest manifest(String group, + String name, + String main, + List subPlugins, + boolean disabledByDefault) { + return new PluginManifest(group, + name, + Semver.fromString("1.2.0"), + "test manifest", + new ArrayList<>(), + "https://example.invalid", + main, + SemverRange.fromString("0.6.0-pre.1"), + new LinkedHashMap<>(), + new LinkedHashMap<>(), + new LinkedHashMap<>(), + new ArrayList<>(subPlugins), + disabledByDefault); + } + + private static PluginManifest decodeGeneratedManifest() throws IOException { + InputStream stream = ImpulseBackendRegistrySubPluginRegistrationTest.class + .getClassLoader() + .getResourceAsStream("manifest.json"); + assertNotNull(stream); + try (InputStreamReader input = new InputStreamReader(stream, StandardCharsets.UTF_8); + RawJsonReader reader = new RawJsonReader(input, RawJsonReader.READ_BUFFER.get())) { + return PluginManifest.CODEC.decodeJson(reader, new ExtraInfo()); + } + } + +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/ImpulseSubPluginRegistrationTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/ImpulseSubPluginRegistrationTest.java deleted file mode 100644 index e03d8c4a..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/ImpulseSubPluginRegistrationTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.hypixel.hytale.codec.ExtraInfo; -import com.hypixel.hytale.codec.util.RawJsonReader; -import com.hypixel.hytale.common.plugin.PluginIdentifier; -import com.hypixel.hytale.common.plugin.PluginManifest; -import com.hypixel.hytale.common.semver.Semver; -import com.hypixel.hytale.common.semver.SemverRange; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import org.junit.jupiter.api.Test; - -class ImpulseSubPluginRegistrationTest { - - @Test - void generatedManifestSubPluginsSupportHytalePendingLoadInheritance() throws IOException { - PluginManifest parent = decodeGeneratedManifest(); - PluginIdentifier parentId = new PluginIdentifier(parent); - - for (PluginManifest subPlugin : parent.getSubPlugins()) { - assertDoesNotThrow(() -> subPlugin.inherit(parent), subPlugin.getName()); - assertTrue(subPlugin.getDependencies().containsKey(parentId)); - } - } - - @Test - void preparesEverySubPluginManifestForDynamicLoad() { - PluginManifest parent = manifest("HytaleModding", - "Impulse", - "dev.hytalemodding.impulse.core.ImpulsePlugin", - List.of( - manifest(null, - "ImpulseWorldCollision", - "dev.hytalemodding.impulse.core.plugin.modules.worldcollision.ImpulseWorldCollisionPlugin", - List.of(), - false), - manifest(null, - "ImpulseControl", - "dev.hytalemodding.impulse.core.plugin.modules.control.ImpulseControlPlugin", - List.of(), - false)), - false); - - List prepared = - ImpulseSubPluginRegistration.prepareSubPluginManifests(parent); - - assertEquals(2, prepared.size()); - assertPreparedSubPlugin(prepared.get(0), "ImpulseWorldCollision", false); - assertPreparedSubPlugin(prepared.get(1), "ImpulseControl", false); - } - - private static void assertPreparedSubPlugin(PluginManifest manifest, - String expectedName, - boolean expectedDisabledByDefault) { - PluginIdentifier parentId = new PluginIdentifier("HytaleModding", "Impulse"); - - assertEquals("HytaleModding", manifest.getGroup()); - assertEquals(expectedName, manifest.getName()); - assertEquals(expectedDisabledByDefault, manifest.isDisabledByDefault()); - assertTrue(manifest.getDependencies().containsKey(parentId)); - } - - private static PluginManifest manifest(String group, - String name, - String main, - List subPlugins, - boolean disabledByDefault) { - return new PluginManifest(group, - name, - Semver.fromString("1.2.0"), - "test manifest", - new ArrayList<>(), - "https://example.invalid", - main, - SemverRange.fromString("0.6.0-pre.1"), - new LinkedHashMap<>(), - new LinkedHashMap<>(), - new LinkedHashMap<>(), - new ArrayList<>(subPlugins), - disabledByDefault); - } - - private static PluginManifest decodeGeneratedManifest() throws IOException { - InputStream stream = ImpulseSubPluginRegistrationTest.class - .getClassLoader() - .getResourceAsStream("manifest.json"); - assertNotNull(stream); - try (InputStreamReader input = new InputStreamReader(stream, StandardCharsets.UTF_8); - RawJsonReader reader = new RawJsonReader(input, RawJsonReader.READ_BUFFER.get())) { - return PluginManifest.CODEC.decodeJson(reader, new ExtraInfo()); - } - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/SubPluginLifecycleGateTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/SubPluginLifecycleGateTest.java index e60dc011..92ba1bea 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/SubPluginLifecycleGateTest.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/SubPluginLifecycleGateTest.java @@ -12,18 +12,18 @@ class SubPluginLifecycleGateTest { @Test void gateStartsDisabledAndFailsFastWithConfiguredMessage() { - SubPluginLifecycleGate gate = new SubPluginLifecycleGate("test module disabled"); + SubPluginLifecycleGate gate = new SubPluginLifecycleGate("test subplugin disabled"); assertFalse(gate.isEnabled()); IllegalStateException exception = assertThrows(IllegalStateException.class, gate::requireEnabled); - assertEquals("test module disabled", exception.getMessage()); + assertEquals("test subplugin disabled", exception.getMessage()); } @Test void generationChangesOnEnableAndDisableTransitions() { - SubPluginLifecycleGate gate = new SubPluginLifecycleGate("test module disabled"); + SubPluginLifecycleGate gate = new SubPluginLifecycleGate("test subplugin disabled"); long initialGeneration = gate.generation(); gate.enable(); @@ -43,7 +43,7 @@ void generationChangesOnEnableAndDisableTransitions() { @Test void disableRunsCleanupCallbacksOnlyWhenTransitioningFromEnabled() { - SubPluginLifecycleGate gate = new SubPluginLifecycleGate("test module disabled"); + SubPluginLifecycleGate gate = new SubPluginLifecycleGate("test subplugin disabled"); AtomicInteger cleanupCount = new AtomicInteger(); gate.onDisable(cleanupCount::incrementAndGet); diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/control/ControlLifecycleTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/control/ControlLifecycleTest.java deleted file mode 100644 index 67ead74a..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/control/ControlLifecycleTest.java +++ /dev/null @@ -1,114 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.control; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.hypixel.hytale.component.ComponentRegistry; -import com.hypixel.hytale.component.EmptyResourceStorage; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.modules.control.components.PhysicsControlSessionComponent; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.modules.control.ImpulseControllableComponent; -import dev.hytalemodding.impulse.core.plugin.modules.control.PhysicsControlSessions; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class ControlLifecycleTest { - - @BeforeEach - @AfterEach - void disableLifecycle() { - ControlLifecycle.disable(); - ImpulseControllableComponent.clearComponentType(); - PhysicsControlSessionComponent.clearComponentType(); - } - - @Test - void lifecycleStartsDisabled() { - assertFalse(ControlLifecycle.isEnabled()); - } - - @Test - void lifecycleGenerationChangesWhenLifecycleIsDisabled() { - ControlLifecycle.enable(); - long enabledGeneration = ControlLifecycle.generation(); - - ControlLifecycle.disable(); - - assertTrue(ControlLifecycle.generation() > enabledGeneration); - } - - @Test - void disablingLifecycleWithoutRegisteredSessionComponentDoesNotThrow() { - PhysicsControlSessionComponent.clearComponentType(); - ControlLifecycle.enable(); - - assertDoesNotThrow(ControlLifecycle::disable); - - assertFalse(ControlLifecycle.isEnabled()); - } - - @Test - void disablingLifecycleClearsRegisteredControlledBodies() { - ControlLifecycle.enable(); - PhysicsWorldRuntimeResource resource = new PhysicsWorldRuntimeResource(); - RigidBodyKey bodyKey = RigidBodyKey.random(); - resource.markBodyControlled(bodyKey); - - assertTrue(resource.isBodyControlled(bodyKey)); - - ControlLifecycle.disable(); - - assertFalse(resource.isBodyControlled(bodyKey)); - } - - @Test - void controlSessionsAreAvailableOnlyWhenLifecycleAndComponentTypesAreRegistered() { - assertFalse(PhysicsControlSessions.isAvailable()); - - ControlLifecycle.enable(); - assertFalse(PhysicsControlSessions.isAvailable()); - - ComponentRegistry registry = new ComponentRegistry<>(); - ImpulseControllableComponent.setComponentType(registry.registerComponent( - ImpulseControllableComponent.class, - "ImpulseControllable", - ImpulseControllableComponent.CODEC)); - PhysicsControlSessionComponent.setComponentType(registry.registerComponent( - PhysicsControlSessionComponent.class, - PhysicsControlSessionComponent::new)); - - assertTrue(PhysicsControlSessions.isAvailable()); - - ControlLifecycle.disable(); - - assertFalse(PhysicsControlSessions.isAvailable()); - } - - @Test - void disablingLifecycleSkipsStoresWhoseWorldThreadHasStopped() { - ControlLifecycle.enable(); - ComponentRegistry registry = new ComponentRegistry<>(); - ImpulseControllableComponent.setComponentType(registry.registerComponent( - ImpulseControllableComponent.class, - "ImpulseControllable", - ImpulseControllableComponent.CODEC)); - PhysicsControlSessionComponent.setComponentType(registry.registerComponent( - PhysicsControlSessionComponent.class, - PhysicsControlSessionComponent::new)); - Store store = registry.addStore( - new EntityStore(TestInstanceFactory.world("stopped-control-world")), - EmptyResourceStorage.get()); - ControlLifecycle.registerStore(store); - - assertDoesNotThrow(ControlLifecycle::disable); - - assertFalse(ControlLifecycle.isEnabled()); - registry.shutdown(); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsKinematicControlSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsKinematicControlSystemTest.java deleted file mode 100644 index e8c6fbc9..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/control/systems/PhysicsKinematicControlSystemTest.java +++ /dev/null @@ -1,449 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.control.systems; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.hypixel.hytale.component.ComponentRegistry; -import com.hypixel.hytale.component.EmptyResourceStorage; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsJoint; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import dev.hytalemodding.impulse.core.internal.modules.control.components.PhysicsControlSessionComponent; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerSnapshot; -import dev.hytalemodding.impulse.core.internal.resources.owner.TestPhysicsOwnerLane; -import dev.hytalemodding.impulse.core.internal.modules.control.systems.PhysicsKinematicControlSystem.ControlAnchorUpdate; -import dev.hytalemodding.impulse.core.internal.modules.control.systems.PhysicsKinematicControlSystem.ControlMutationState; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import java.time.Duration; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import javax.annotation.Nonnull; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class PhysicsKinematicControlSystemTest { - - private static final AtomicInteger BACKEND_COUNTER = new AtomicInteger(); - - @Test - void controlAnchorUpdateCopiesMutableVectors() { - RigidBodyKey bodyId = RigidBodyKey.random(); - RigidBodyKey anchorBodyId = RigidBodyKey.random(); - Vector3f target = new Vector3f(1.0f, 2.0f, 3.0f); - Vector3f releaseVelocity = new Vector3f(4.0f, 5.0f, 6.0f); - ControlAnchorUpdate update = new ControlAnchorUpdate(bodyId, - anchorBodyId, - target, - releaseVelocity); - target.zero(); - releaseVelocity.zero(); - - assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), update.target()); - assertEquals(new Vector3f(4.0f, 5.0f, 6.0f), update.releaseVelocity()); - } - - @Test - void pendingControlMutationBlocksAnotherSubmissionUntilComplete() { - ControlMutationState state = new ControlMutationState(); - RigidBodyKey bodyId = RigidBodyKey.random(); - CompletableFuture completion = new CompletableFuture<>(); - PhysicsMutationHandle handle = PhysicsMutationHandle.fromCompletion("test", - null, - completion); - ControlAnchorUpdate first = update(bodyId, bodyId, 1.0f); - ControlAnchorUpdate second = update(bodyId, bodyId, 2.0f); - - state.trackPendingMutation(bodyId, handle, first); - - assertTrue(state.hasPendingMutation(bodyId)); - assertNull(state.selectReadyUpdate(bodyId, second)); - - completion.complete(null); - - assertFalse(state.hasPendingMutation(bodyId)); - } - - @Test - void clearingControlMutationStateAllowsImmediateRetry() { - ControlMutationState state = new ControlMutationState(); - RigidBodyKey bodyId = RigidBodyKey.random(); - CompletableFuture completion = new CompletableFuture<>(); - PhysicsMutationHandle handle = PhysicsMutationHandle.fromCompletion("test", - null, - completion); - - state.trackPendingMutation(bodyId, handle, update(bodyId, bodyId, 1.0f)); - state.clear(bodyId); - - assertFalse(state.hasPendingMutation(bodyId)); - - completion.complete(null); - assertFalse(state.hasPendingMutation(bodyId)); - } - - @Test - void pendingControlMutationCoalescesLatestTargetUntilCompletion() { - ControlMutationState state = new ControlMutationState(); - RigidBodyKey bodyId = RigidBodyKey.random(); - RigidBodyKey anchorBodyId = RigidBodyKey.random(); - CompletableFuture completion = new CompletableFuture<>(); - PhysicsMutationHandle handle = PhysicsMutationHandle.fromCompletion("test", - null, - completion); - ControlAnchorUpdate first = update(bodyId, anchorBodyId, 1.0f); - ControlAnchorUpdate second = update(bodyId, anchorBodyId, 2.0f); - ControlAnchorUpdate third = update(bodyId, anchorBodyId, 3.0f); - ControlAnchorUpdate current = update(bodyId, anchorBodyId, 4.0f); - - state.trackPendingMutation(anchorBodyId, handle, first); - - assertNull(state.selectReadyUpdate(anchorBodyId, second)); - assertNull(state.selectReadyUpdate(anchorBodyId, third)); - - completion.complete(null); - ControlAnchorUpdate ready = state.selectReadyUpdate(anchorBodyId, current); - - assertSame(third, ready); - } - - @Test - void clearingSystemMutationStateAfterReleaseAllowsIdenticalTargetRetry() { - ComponentRegistry registry = new ComponentRegistry<>(); - Store store = registry.addStore( - new EntityStore(TestInstanceFactory.world("control-release-mutation-state-test")), - EmptyResourceStorage.get()); - try { - ControlMutationState state = PhysicsKinematicControlSystem.stateFor(store); - RigidBodyKey bodyId = RigidBodyKey.random(); - RigidBodyKey anchorBodyId = RigidBodyKey.random(); - CompletableFuture completion = new CompletableFuture<>(); - PhysicsMutationHandle handle = PhysicsMutationHandle.fromCompletion("test", - null, - completion); - ControlAnchorUpdate first = update(bodyId, anchorBodyId, 1.0f); - ControlAnchorUpdate queued = update(bodyId, anchorBodyId, 1.0f); - ControlAnchorUpdate afterRelease = update(bodyId, anchorBodyId, 1.0f); - - state.trackPendingMutation(anchorBodyId, handle, first); - assertNull(state.selectReadyUpdate(anchorBodyId, queued)); - completion.complete(null); - - PhysicsKinematicControlSystem.clearMutationState(store, anchorBodyId); - - assertSame(afterRelease, state.selectReadyUpdate(anchorBodyId, afterRelease)); - } finally { - registry.removeStore(store); - registry.shutdown(); - } - } - - @Test - void pendingAnchorCreationIsUsableBeforePublishedRegistrationViewArrives() throws Exception { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:control-pending-anchor-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("control-pending-anchor"); - resource.attachOwnerExecutor(owner); - PhysicsSpace space = resource.createLiveSpace(backend.getId()); - RigidBodyKey anchorBodyId = RigidBodyKey.random(); - - resource.submitCommands(10L, commands -> commands - .spawnBody(anchorBodyId, spawn -> spawn - .space(space.id()) - .sphere(0.08f) - .kinematic() - .temporary() - .runtimeOnly())) - .completionSummary() - .toCompletableFuture() - .get(2L, TimeUnit.SECONDS); - - assertNull(resource.getBodyRegistrationView(anchorBodyId)); - assertTrue(resource.hasPublishedOrPendingBodyRegistration(anchorBodyId)); - - resource.detachOwnerExecutor(owner); - } - } - - @Test - void controlJointCleanupResolvesJointFromBodyIds() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:control-joint-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId()); - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody anchorBody = space.createSphere(0.1f, 1.0f); - RigidBodyKey bodyId = resource.addBody(space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - RigidBodyKey anchorBodyId = resource.addBody(space.id(), - anchorBody, - PhysicsBodyKind.TEMPORARY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - space.createPointJoint(anchorBody, body, new Vector3f(), new Vector3f()); - - assertEquals(1, space.jointCount()); - - boolean removed = resource.callOwner("remove control joint", () -> { - if (space.getJoints().isEmpty()) { - return false; - } - space.removeJoint(space.getJoints().getFirst()); - return true; - }); - assertTrue(removed); - - assertEquals(0, space.jointCount()); - } - - @Test - void sessionCleanupDestroysAnchorBodyAndClearsControlledState() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:control-cleanup-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId()); - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody anchorBody = space.createSphere(0.1f, 1.0f); - RigidBodyKey bodyId = resource.addBody(space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - RigidBodyKey anchorBodyId = resource.addBody(space.id(), - anchorBody, - PhysicsBodyKind.TEMPORARY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - PhysicsJoint controlJoint = - space.createPointJoint(anchorBody, body, new Vector3f(), new Vector3f()); - JointKey controlJointId = resource.addJoint(space.id(), controlJoint); - resource.markBodyControlled(bodyId); - PhysicsControlSessionComponent session = new PhysicsControlSessionComponent(bodyId, - anchorBodyId, - controlJointId, - null, - space.id(), - body.getBodyType(), - 4.0f, - new Vector3f(), - new Vector3f()); - - assertEquals(controlJointId, session.getControlJointKey()); - assertEquals(controlJointId, session.clone().getControlJointKey()); - assertSame(controlJoint, resource.getJoint(controlJointId)); - - PhysicsControlSessionCleanup.cleanup(resource, session); - - assertFalse(resource.isBodyControlled(bodyId)); - assertNull(resource.getJoint(controlJointId)); - assertEquals(1, space.bodyCount()); - assertEquals(0, space.jointCount()); - assertTrue(space.containsBody(body)); - assertFalse(space.containsBody(anchorBody)); - assertEquals(body, resource.getBody(bodyId)); - assertNull(resource.getBodyRegistrationView(anchorBodyId)); - } - - @Test - void sessionCleanupRestoresOriginalBodyState() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:control-cleanup-state-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId()); - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody anchorBody = space.createSphere(0.1f, 1.0f); - RigidBodyKey bodyId = resource.addBody(space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - RigidBodyKey anchorBodyId = resource.addBody(space.id(), - anchorBody, - PhysicsBodyKind.TEMPORARY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - PhysicsJoint controlJoint = - space.createPointJoint(anchorBody, body, new Vector3f(), new Vector3f()); - JointKey controlJointId = resource.addJoint(space.id(), controlJoint); - body.setBodyType(PhysicsBodyType.KINEMATIC); - body.setLinearVelocity(0.0f, 0.0f, 0.0f); - PhysicsControlSessionComponent session = new PhysicsControlSessionComponent(bodyId, - anchorBodyId, - controlJointId, - null, - space.id(), - PhysicsBodyType.DYNAMIC, - 4.0f, - new Vector3f(), - new Vector3f()); - session.getReleaseVelocity().set(3.0f, 4.0f, 5.0f); - - PhysicsControlSessionCleanup.cleanup(resource, session); - - assertEquals(PhysicsBodyType.DYNAMIC, body.getBodyType()); - assertEquals(new Vector3f(3.0f, 4.0f, 5.0f), body.getLinearVelocity()); - } - - @Test - void sessionCleanupQueuesOwnerReleaseWithoutBlocking() throws Exception { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:control-cleanup-owner-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId()); - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody anchorBody = space.createSphere(0.1f, 1.0f); - RigidBodyKey bodyId = resource.addBody(space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - RigidBodyKey anchorBodyId = resource.addBody(space.id(), - anchorBody, - PhysicsBodyKind.TEMPORARY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - PhysicsJoint controlJoint = - space.createPointJoint(anchorBody, body, new Vector3f(), new Vector3f()); - JointKey controlJointId = resource.addJoint(space.id(), controlJoint); - resource.markBodyControlled(bodyId); - PhysicsControlSessionComponent session = new PhysicsControlSessionComponent(bodyId, - anchorBodyId, - controlJointId, - null, - space.id(), - body.getBodyType(), - 4.0f, - new Vector3f(), - new Vector3f()); - - CountDownLatch mutationStarted = new CountDownLatch(1); - CountDownLatch releaseMutation = new CountDownLatch(1); - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("control-cleanup-owner"); - resource.attachOwnerExecutor(owner); - owner.submitMutation("blocking mutation", () -> { - mutationStarted.countDown(); - assertTrue(releaseMutation.await(2L, TimeUnit.SECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(mutationStarted.await(2L, TimeUnit.SECONDS)); - - CompletableFuture cleanup = CompletableFuture.runAsync( - () -> PhysicsControlSessionCleanup.cleanup(resource, session)); - cleanup.get(200L, TimeUnit.MILLISECONDS); - - assertFalse(resource.isBodyControlled(bodyId)); - assertEquals(2, owner.pendingMutations()); - - releaseMutation.countDown(); - pollMutationCompletions(owner, 2); - resource.detachOwnerExecutor(owner); - } finally { - releaseMutation.countDown(); - } - } - - @Test - void lifecycleSessionCleanupWaitsForOwnerRelease() throws Exception { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:control-cleanup-lifecycle-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId()); - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody anchorBody = space.createSphere(0.1f, 1.0f); - RigidBodyKey bodyId = resource.addBody(space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - RigidBodyKey anchorBodyId = resource.addBody(space.id(), - anchorBody, - PhysicsBodyKind.TEMPORARY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - PhysicsJoint controlJoint = - space.createPointJoint(anchorBody, body, new Vector3f(), new Vector3f()); - JointKey controlJointId = resource.addJoint(space.id(), controlJoint); - PhysicsControlSessionComponent session = new PhysicsControlSessionComponent(bodyId, - anchorBodyId, - controlJointId, - null, - space.id(), - body.getBodyType(), - 4.0f, - new Vector3f(), - new Vector3f()); - - CountDownLatch mutationStarted = new CountDownLatch(1); - CountDownLatch releaseMutation = new CountDownLatch(1); - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("control-cleanup-lifecycle"); - resource.attachOwnerExecutor(owner); - owner.submitMutation("blocking mutation", () -> { - mutationStarted.countDown(); - assertTrue(releaseMutation.await(2L, TimeUnit.SECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(mutationStarted.await(2L, TimeUnit.SECONDS)); - - CompletableFuture cleanup = CompletableFuture.runAsync( - () -> PhysicsControlSessionCleanup.cleanupAndWait(resource, session)); - - Thread.sleep(100L); - assertFalse(cleanup.isDone()); - - releaseMutation.countDown(); - cleanup.get(2L, TimeUnit.SECONDS); - - assertTrue(resource.callOwner("verify lifecycle control cleanup", - () -> resource.getJoint(controlJointId) == null - && !space.containsBody(anchorBody))); - resource.detachOwnerExecutor(owner); - } finally { - releaseMutation.countDown(); - } - } - - @Nonnull - private static ControlAnchorUpdate update(@Nonnull RigidBodyKey bodyId, - @Nonnull RigidBodyKey anchorBodyId, - float coordinate) { - return new ControlAnchorUpdate(bodyId, - anchorBodyId, - new Vector3f(coordinate, coordinate, coordinate), - new Vector3f(coordinate + 1.0f, coordinate + 1.0f, coordinate + 1.0f)); - } - - private static void pollMutationCompletions(@Nonnull TestPhysicsOwnerLane owner, - int expected) throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2L); - int completed = 0; - while (System.nanoTime() < deadline) { - completed += owner.pollCompletedMutations(8).size(); - if (completed >= expected) { - return; - } - Thread.sleep(10L); - } - assertEquals(expected, completed); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkCollisionStreamingResourceTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkCollisionStreamingResourceTest.java new file mode 100644 index 00000000..794e4fad --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkCollisionStreamingResourceTest.java @@ -0,0 +1,100 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkMutationCache.TargetRefreshDecision; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkMutationCache.TargetRefreshReason; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import java.util.UUID; +import org.junit.jupiter.api.Test; + +class PhysicsChunkCollisionStreamingResourceTest { + + @Test + void clonePreservesDynamicBodyStreamingTargetState() { + UUID spaceUuid = uuid(1); + UUID bodyUuid = uuid(2); + PhysicsChunkStreamingBounds bounds = PhysicsChunkStreamingBounds.from(10.0f, + 65.0f, + -4.0f, + 4); + PhysicsChunkCollisionStreamingResource resource = + new PhysicsChunkCollisionStreamingResource(); + + TargetRefreshDecision initial = resource.shouldRefreshBodyTarget(spaceUuid, + bodyUuid, + bounds, + false, + 1L, + 100, + null); + assertTrue(initial.refresh()); + resource.recordBodyTargetRefresh(spaceUuid, bodyUuid, bounds, false, 1L); + + PhysicsChunkCollisionStreamingResource copy = resource.clone(); + TargetRefreshDecision copied = copy.shouldRefreshBodyTarget(spaceUuid, + bodyUuid, + bounds, + false, + 2L, + 100, + null); + + assertFalse(copied.refresh()); + assertEquals(TargetRefreshReason.STABLE_SKIP, copied.reason()); + } + + @Test + void clonePreservesRefBodyStreamingTargetState() { + ComponentRegistry registry = new ComponentRegistry<>(); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("physicschunk-streaming-resource-ref-clone")), + EmptyResourceStorage.get()); + try { + UUID spaceUuid = uuid(3); + Ref bodyRef = new Ref<>(store, 11); + PhysicsChunkStreamingBounds bounds = PhysicsChunkStreamingBounds.from(32.0f, + 65.0f, + 16.0f, + 4); + PhysicsChunkCollisionStreamingResource resource = + new PhysicsChunkCollisionStreamingResource(); + + TargetRefreshDecision initial = resource.shouldRefreshBodyTarget(spaceUuid, + bodyRef, + bounds, + false, + 1L, + 100, + null); + assertTrue(initial.refresh()); + resource.recordBodyTargetRefresh(spaceUuid, bodyRef, bounds, false, 1L); + + PhysicsChunkCollisionStreamingResource copy = resource.clone(); + TargetRefreshDecision copied = copy.shouldRefreshBodyTarget(spaceUuid, + bodyRef, + bounds, + false, + 2L, + 100, + null); + + assertFalse(copied.refresh()); + assertEquals(TargetRefreshReason.STABLE_SKIP, copied.reason()); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + private static UUID uuid(long leastSignificantBits) { + return new UUID(0L, leastSignificantBits); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkLifecycleTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkLifecycleTest.java new file mode 100644 index 00000000..3319a531 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkLifecycleTest.java @@ -0,0 +1,33 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class PhysicsChunkLifecycleTest { + + @BeforeEach + @AfterEach + void disableLifecycle() { + PhysicsChunkLifecycle.disable(); + } + + @Test + void lifecycleStartsDisabled() { + assertFalse(PhysicsChunkLifecycle.isEnabled()); + } + + @Test + void lifecycleGenerationChangesWhenLifecycleIsDisabled() { + PhysicsChunkLifecycle.enable(); + long enabledGeneration = PhysicsChunkLifecycle.generation(); + + PhysicsChunkLifecycle.disable(); + + assertFalse(PhysicsChunkLifecycle.isEnabled()); + assertTrue(PhysicsChunkLifecycle.generation() > enabledGeneration); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkStoreTypesTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkStoreTypesTest.java new file mode 100644 index 00000000..519ecb0f --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkStoreTypesTest.java @@ -0,0 +1,70 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.PhysicsStoreRegistration; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionMutationQueueResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionPayloadResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkComponentSyncResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkSettingsIndexResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import java.util.ArrayList; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +class PhysicsChunkStoreTypesTest { + + @Test + void runtimeResourceCleanupIgnoresUnregisteredPhysicsChunkResources() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + PhysicsChunkStoreTypes.registerPhysicsStoreResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("physicschunk-store-resource-cleanup")), + EmptyResourceStorage.get()); + try { + unregisterPhysicsChunkResources(registry); + + assertDoesNotThrow(() -> PhysicsChunkStoreTypes.clearPhysicsStoreRuntimeResources(store)); + } finally { + PhysicsChunkStoreTypes.clearPhysicsStoreResourceTypes(); + registry.removeStore(store); + } + } + + @Test + void physicsChunkSystemsRegisterInPluginSetupOrder() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + try { + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsChunkStoreTypes.registerPhysicsStoreResourceTypes(proxy); + PhysicsStoreRegistration.register(proxy); + PhysicsChunkStoreTypes.registerSpaceBindingSystems(proxy); + + assertDoesNotThrow(() -> PhysicsChunkStoreTypes.registerPreBodyBindingSystems(proxy)); + assertDoesNotThrow(() -> PhysicsChunkStoreTypes.registerPostBodyBindingSystems(proxy)); + } finally { + PhysicsChunkStoreTypes.clearPhysicsStoreResourceTypes(); + registry.shutdown(); + } + } + + private static void unregisterPhysicsChunkResources(@Nonnull ComponentRegistry registry) { + registry.unregisterResource(PhysicsChunkCollisionMutationQueueResource.getResourceType()); + registry.unregisterResource(PhysicsChunkCollisionPayloadResource.getResourceType()); + registry.unregisterResource(PhysicsChunkSettingsIndexResource.getResourceType()); + registry.unregisterResource(PhysicsChunkComponentSyncResource.getResourceType()); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldCollisionStreamingBoundsTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkStreamingBoundsTest.java similarity index 63% rename from impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldCollisionStreamingBoundsTest.java rename to impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkStreamingBoundsTest.java index 8a7da48a..8395735c 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldCollisionStreamingBoundsTest.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/PhysicsChunkStreamingBoundsTest.java @@ -1,20 +1,20 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision; +package dev.hytalemodding.impulse.core.internal.modules.physicschunk; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldCollisionStreamingBounds; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkStreamingBounds; import org.joml.Vector3f; import org.junit.jupiter.api.Test; -class WorldCollisionStreamingBoundsTest { +class PhysicsChunkStreamingBoundsTest { @Test void dedupesBodiesThatShareTheSameChunkAndSectionNeighborhood() { - WorldCollisionStreamingBounds first = WorldCollisionStreamingBounds.from( + PhysicsChunkStreamingBounds first = PhysicsChunkStreamingBounds.from( new Vector3f(10.2f, 65.4f, -3.7f), 4); - WorldCollisionStreamingBounds second = WorldCollisionStreamingBounds.from( + PhysicsChunkStreamingBounds second = PhysicsChunkStreamingBounds.from( new Vector3f(10.8f, 65.1f, -3.2f), 4); @@ -24,10 +24,10 @@ void dedupesBodiesThatShareTheSameChunkAndSectionNeighborhood() { @Test void keepsBodiesDistinctWhenTheirChunkNeighborhoodChanges() { - WorldCollisionStreamingBounds first = WorldCollisionStreamingBounds.from( + PhysicsChunkStreamingBounds first = PhysicsChunkStreamingBounds.from( new Vector3f(15.5f, 70.0f, 15.5f), 4); - WorldCollisionStreamingBounds second = WorldCollisionStreamingBounds.from( + PhysicsChunkStreamingBounds second = PhysicsChunkStreamingBounds.from( new Vector3f(31.5f, 70.0f, 15.5f), 4); @@ -37,7 +37,7 @@ void keepsBodiesDistinctWhenTheirChunkNeighborhoodChanges() { @Test void clampsVerticalBoundsToValidSectionRange() { - WorldCollisionStreamingBounds bounds = WorldCollisionStreamingBounds.from( + PhysicsChunkStreamingBounds bounds = PhysicsChunkStreamingBounds.from( new Vector3f(0.0f, -20.0f, 0.0f), 12); @@ -48,10 +48,10 @@ void clampsVerticalBoundsToValidSectionRange() { @Test void scalarFactoryMatchesVectorFactory() { - WorldCollisionStreamingBounds vectorBounds = WorldCollisionStreamingBounds.from( + PhysicsChunkStreamingBounds vectorBounds = PhysicsChunkStreamingBounds.from( new Vector3f(10.2f, 65.4f, -3.7f), 4); - WorldCollisionStreamingBounds scalarBounds = WorldCollisionStreamingBounds.from( + PhysicsChunkStreamingBounds scalarBounds = PhysicsChunkStreamingBounds.from( 10.2f, 65.4f, -3.7f, diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkPerfReportCommandTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkPerfReportCommandTest.java new file mode 100644 index 00000000..6b21e17c --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/commands/PhysicsChunkPerfReportCommandTest.java @@ -0,0 +1,132 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsRuntimeProfiling.StepDrainSnapshotView; +import org.junit.jupiter.api.Test; + +class PhysicsChunkPerfReportCommandTest { + + @Test + void runtimeStatsSummaryIncludesLiveBackendPressureCounters() { + RuntimeStatsSample sample = new RuntimeStatsSample(2, + 180, + 210, + 24, + 96, + 44, + 128, + 4, + 92, + 3, + 12); + + assertEquals("spaces=2 bodies=180 colliders=210 activeBodies=24 activeIslands=3 " + + "contactPairs=96 contactManifolds=44 contactPoints=128 " + + "dynamicDynamicPairs=4 terrainPairs=92 joints=12", + PhysicsChunkPerfReportCommand.formatRuntimeStatsSummary(sample)); + } + + @Test + void preStepDrainSummaryReportsAverageLatestAndMaxBackpressure() { + StepDrainSample cumulative = new StepDrainSample(2, + 6, + 4, + 12_000_000L, + 4, + 3); + StepDrainSample latest = new StepDrainSample(1, + 4, + 4, + 8_000_000L, + 3, + 3); + + assertEquals("Physics pre-step drain avg completedStep drained/runMs/lateBacklog=3.0/6.000/2.0 " + + "latest drained/lateBacklog=4/3 max drained/lateBacklog=4/3", + PhysicsChunkPerfReportCommand.formatPreStepDrainSummary(cumulative, latest)); + } + + @Test + void preStepDrainSummaryRequiresCompletedStepSamples() { + assertFalse(PhysicsChunkPerfReportCommand.hasCompletedStepSamples( + new StepDrainSample(0, 0, 0, 0L, 0, 0))); + + assertTrue(PhysicsChunkPerfReportCommand.hasCompletedStepSamples( + new StepDrainSample(1, 1, 1, 2_000_000L, 0, 0))); + } + + @Test + void preStepDrainSummaryUsesLatestCompletedStepAfterSkippedPendingTick() { + StepDrainSample cumulative = new StepDrainSample(1, + 4, + 4, + 8_000_000L, + 3, + 3); + StepDrainSample latestCompleted = new StepDrainSample(1, + 4, + 4, + 8_000_000L, + 3, + 3); + + assertEquals("Physics pre-step drain avg completedStep drained/runMs/lateBacklog=4.0/8.000/3.0 " + + "latest drained/lateBacklog=4/3 max drained/lateBacklog=4/3", + PhysicsChunkPerfReportCommand.formatPreStepDrainSummary(cumulative, + latestCompleted)); + } + + private record StepDrainSample(int tickSamples, + int preStepDrainedMutations, + int maxPreStepDrainedMutations, + long preStepDrainRunNanos, + int lateMutationBacklogAtStep, + int maxLateMutationBacklogAtStep) implements StepDrainSnapshotView { + + @Override + public int getTickSamples() { + return tickSamples; + } + + @Override + public int getPreStepDrainedMutations() { + return preStepDrainedMutations; + } + + @Override + public int getMaxPreStepDrainedMutations() { + return maxPreStepDrainedMutations; + } + + @Override + public long getPreStepDrainRunNanos() { + return preStepDrainRunNanos; + } + + @Override + public int getLateMutationBacklogAtStep() { + return lateMutationBacklogAtStep; + } + + @Override + public int getMaxLateMutationBacklogAtStep() { + return maxLateMutationBacklogAtStep; + } + } + + private record RuntimeStatsSample(int runtimeStatsSpaces, + int runtimeBodies, + int runtimeColliders, + int runtimeActiveBodies, + int runtimeContactPairs, + int runtimeContactManifolds, + int runtimeContactPoints, + int runtimeDynamicDynamicContactPairs, + int runtimeTerrainContactPairs, + int runtimeActiveIslands, + int runtimeJoints) implements PhysicsChunkPerfReportCommand.RuntimeStatsView { + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/profiling/WorldCollisionProfilingResourceTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/profiling/PhysicsChunkProfilingResourceTest.java similarity index 75% rename from impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/profiling/WorldCollisionProfilingResourceTest.java rename to impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/profiling/PhysicsChunkProfilingResourceTest.java index 6024c0a4..670847f0 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/profiling/WorldCollisionProfilingResourceTest.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/profiling/PhysicsChunkProfilingResourceTest.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling; +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.profiling; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -9,45 +9,44 @@ import com.hypixel.hytale.component.ComponentRegistry; import com.hypixel.hytale.component.ResourceType; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldVoxelCollisionCache.BuildStats; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource.MissingSectionReason; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkBuildStats; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.profiling.PhysicsChunkProfilingResource.MissingSectionReason; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; -class WorldCollisionProfilingResourceTest { +class PhysicsChunkProfilingResourceTest { @AfterEach void clearRegistration() { - WorldCollisionProfilingResource.clearResourceType(); + PhysicsChunkProfilingResource.clearResourceType(); } @Test - void resourceTypeIsOwnedByWorldCollisionModuleRegistration() { - assertFalse(WorldCollisionProfilingResource.isResourceTypeRegistered()); - assertThrows(IllegalStateException.class, WorldCollisionProfilingResource::getResourceType); + void resourceTypeIsOwnedByPhysicsChunkSubPluginRegistration() { + assertFalse(PhysicsChunkProfilingResource.isResourceTypeRegistered()); + assertThrows(IllegalStateException.class, PhysicsChunkProfilingResource::getResourceType); ComponentRegistry registry = new ComponentRegistry<>(); - ResourceType type = - registry.registerResource(WorldCollisionProfilingResource.class, - WorldCollisionProfilingResource::new); + ResourceType type = + registry.registerResource(PhysicsChunkProfilingResource.class, + PhysicsChunkProfilingResource::new); - WorldCollisionProfilingResource.setResourceType(type); + PhysicsChunkProfilingResource.setResourceType(type); - assertTrue(WorldCollisionProfilingResource.isResourceTypeRegistered()); - assertSame(type, WorldCollisionProfilingResource.getResourceType()); + assertTrue(PhysicsChunkProfilingResource.isResourceTypeRegistered()); + assertSame(type, PhysicsChunkProfilingResource.getResourceType()); - WorldCollisionProfilingResource.clearResourceType(); + PhysicsChunkProfilingResource.clearResourceType(); - assertFalse(WorldCollisionProfilingResource.isResourceTypeRegistered()); + assertFalse(PhysicsChunkProfilingResource.isResourceTypeRegistered()); } @Test void finishTickTracksLatestCumulativeAndWorstSnapshots() { - WorldCollisionProfilingResource resource = new WorldCollisionProfilingResource(); + PhysicsChunkProfilingResource resource = new PhysicsChunkProfilingResource(); - WorldCollisionProfilingResource.Snapshot first = resource.beginTick(); + PhysicsChunkProfilingResource.Snapshot first = resource.beginTick(); first.setPlayerStreamingTargets(2); first.addBodyStreamingCandidates(5); first.addBodyStreamingTargets(3); @@ -61,8 +60,8 @@ void finishTickTracksLatestCumulativeAndWorstSnapshots() { first.incrementBodyTargetSleepingStableSkips(); first.addBodyTargetsPruned(2); first.incrementStreamingSpaces(); - first.incrementTerrainApplyQueued(); - first.incrementTerrainApplySkippedPending(); + first.incrementCollisionApplyQueued(); + first.incrementCollisionApplySkippedPending(); first.incrementEnsureCalls(); first.incrementSectionRequests(); first.incrementSectionCacheHits(); @@ -72,7 +71,7 @@ void finishTickTracksLatestCumulativeAndWorstSnapshots() { first.recordMissingSection(MissingSectionReason.BLOCK_CHUNK, 1, 2, 3, null); first.recordMissingSection(MissingSectionReason.BLOCK_SECTION, 1, 2, 3, null); first.incrementDuplicateSkips(); - first.addBuildStats(new BuildStats(10, 8, 2, 3, 4, 1, 1, 2, 1, 1)); + first.addBuildStats(new PhysicsChunkBuildStats(10, 8, 2, 3, 4, 1, 1, 2, 1, 1)); first.addUnloadedPrune(2, 3); first.addTtlPrune(1, 4); first.addEnsureAroundNanos(20L); @@ -82,7 +81,7 @@ void finishTickTracksLatestCumulativeAndWorstSnapshots() { first.setTickNanos(120L); resource.finishTick(first); - WorldCollisionProfilingResource.Snapshot second = resource.beginTick(); + PhysicsChunkProfilingResource.Snapshot second = resource.beginTick(); second.setPlayerStreamingTargets(1); second.addBodyStreamingCandidates(2); second.addBodyStreamingTargets(1); @@ -108,8 +107,8 @@ void finishTickTracksLatestCumulativeAndWorstSnapshots() { assertEquals(1, resource.getCumulative().getBodyTargetSleepingStableSkips()); assertEquals(2, resource.getCumulative().getBodyTargetsPruned()); assertEquals(1, resource.getCumulative().getStreamingSpaces()); - assertEquals(1, resource.getCumulative().getTerrainApplyQueued()); - assertEquals(1, resource.getCumulative().getTerrainApplySkippedPending()); + assertEquals(1, resource.getCumulative().getCollisionApplyQueued()); + assertEquals(1, resource.getCumulative().getCollisionApplySkippedPending()); assertEquals(1, resource.getCumulative().getEnsureCalls()); assertEquals(1, resource.getCumulative().getSectionRequests()); assertEquals(1, resource.getCumulative().getSectionCacheHits()); @@ -148,13 +147,13 @@ void finishTickTracksLatestCumulativeAndWorstSnapshots() { @Test void cloneAndResetPreserveExpectedMetrics() { - WorldCollisionProfilingResource resource = new WorldCollisionProfilingResource(); + PhysicsChunkProfilingResource resource = new PhysicsChunkProfilingResource(); resource.setEnabled(true); - WorldCollisionProfilingResource.Snapshot snapshot = resource.beginTick(); + PhysicsChunkProfilingResource.Snapshot snapshot = resource.beginTick(); snapshot.setTickNanos(25L); resource.finishTick(snapshot); - WorldCollisionProfilingResource copy = resource.clone(); + PhysicsChunkProfilingResource copy = resource.clone(); assertTrue(copy.isEnabled()); assertEquals(25L, copy.getLatestTick().getTickNanos()); assertEquals(25L, copy.getWorstTick().getTickNanos()); @@ -167,17 +166,17 @@ void cloneAndResetPreserveExpectedMetrics() { @Test void missingSectionDiagnosticsTrackRetainedEnvelopeStatus() { - WorldCollisionProfilingResource resource = new WorldCollisionProfilingResource(); + PhysicsChunkProfilingResource resource = new PhysicsChunkProfilingResource(); LongOpenHashSet retained = new LongOpenHashSet(); - retained.add(WorldCollisionProfilingResource.packDiagnosticSectionKey(1, 2, 3)); + retained.add(PhysicsChunkProfilingResource.packDiagnosticSectionKey(1, 2, 3)); resource.setDiagnosticRetainedSections(retained); - WorldCollisionProfilingResource.Snapshot snapshot = resource.beginTick(); + PhysicsChunkProfilingResource.Snapshot snapshot = resource.beginTick(); snapshot.recordMissingSection(MissingSectionReason.BLOCK_CHUNK, 1, 2, 3, null); snapshot.recordMissingSection(MissingSectionReason.BLOCK_SECTION, 4, 5, 6, null); resource.finishTick(snapshot); - WorldCollisionProfilingResource.Snapshot cumulative = resource.getCumulativeSnapshot(); + PhysicsChunkProfilingResource.Snapshot cumulative = resource.getCumulativeSnapshot(); assertEquals(2, cumulative.getMissingChunks()); assertEquals(1, cumulative.getMissingBlockChunks()); assertEquals(1, cumulative.getMissingBlockSections()); @@ -186,9 +185,9 @@ void missingSectionDiagnosticsTrackRetainedEnvelopeStatus() { assertEquals(1, cumulative.getMissingOutsideRetainedEnvelope()); assertEquals(0, cumulative.getMissingUnconfiguredRetainedEnvelope()); assertEquals(2, cumulative.getMissingSectionSamples().size()); - assertEquals(WorldCollisionProfilingResource.RetainedEnvelopeStatus.INSIDE, + assertEquals(PhysicsChunkProfilingResource.RetainedEnvelopeStatus.INSIDE, cumulative.getMissingSectionSamples().get(0).retainedEnvelopeStatus()); - assertEquals(WorldCollisionProfilingResource.RetainedEnvelopeStatus.OUTSIDE, + assertEquals(PhysicsChunkProfilingResource.RetainedEnvelopeStatus.OUTSIDE, cumulative.getMissingSectionSamples().get(1).retainedEnvelopeStatus()); } } diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionComponentSyncSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionComponentSyncSystemTest.java new file mode 100644 index 00000000..a700e058 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionComponentSyncSystemTest.java @@ -0,0 +1,297 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.PhysicsAxis; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.FakePhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkStoreTypes; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent.PartKind; +import dev.hytalemodding.impulse.core.internal.systems.IdentityIndexSystem; +import dev.hytalemodding.impulse.core.internal.systems.PersistenceHydrationSystem; +import dev.hytalemodding.impulse.core.internal.systems.SpaceSettingsApplicationSystem; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.systems.binding.BodyBindingSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.SpaceBindingSystem; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import java.util.ArrayList; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class ChunkCollisionComponentSyncSystemTest { + + private static final float DELTA = 0.000001f; + + @Test + void spaceSurfaceComponentsSyncGeneratedRowsAndBoundBackends() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + PhysicsChunkStoreTypes.registerPhysicsStoreResourceTypes(proxy); + proxy.registerSystem(new PersistenceHydrationSystem()); + proxy.registerSystem(new IdentityIndexSystem()); + proxy.registerSystem(new SpaceBindingSystem()); + proxy.registerSystem(new SpaceSettingsApplicationSystem()); + proxy.registerSystem(new BodyBindingSystem()); + proxy.registerSystem(new PhysicsChunkSettingsIndexSystem()); + proxy.registerSystem(new ChunkCollisionMutationDrainSystem()); + proxy.registerSystem(new ChunkCollisionComponentSyncSystem()); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("chunk-collision-component-sync-test")), + EmptyResourceStorage.get()); + try { + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + restore.markComplete(); + restore.markHydrated(); + UUID spaceUuid = uuid(1); + RuntimeFixture runtime = addBoundSpace(store, + spaceUuid, + new BackendId("test:chunk-collision-component-sync")); + MaterialComponent expectedMaterial = new MaterialComponent(0.84f, 0.16f); + CollisionFilterComponent expectedFilter = new CollisionFilterComponent(0x40, 0x07); + putSpaceSurface(store, runtime.spaceRef(), expectedMaterial, expectedFilter); + + GeneratedRow boundRow = addGeneratedBoxRow(store, runtime, spaceUuid, "0:0:0", true); + GeneratedRow unboundRow = addGeneratedBoxRow(store, runtime, spaceUuid, "1:0:0", false); + + store.tick(0.0f); + + assertSurface(store, boundRow.ref(), expectedMaterial, expectedFilter); + assertSurface(store, unboundRow.ref(), expectedMaterial, expectedFilter); + assertEquals(expectedMaterial.getFriction(), + runtime.backendRuntime().bodyFriction(runtime.spaceHandle().value(), + boundRow.bodyHandle().value()), + DELTA); + assertEquals(expectedMaterial.getRestitution(), + runtime.backendRuntime().bodyRestitution(runtime.spaceHandle().value(), + boundRow.bodyHandle().value()), + DELTA); + assertEquals(expectedFilter.getCollisionGroup(), + runtime.backendRuntime().bodyCollisionGroup(runtime.spaceHandle().value(), + boundRow.bodyHandle().value())); + assertEquals(expectedFilter.getCollisionMask(), + runtime.backendRuntime().bodyCollisionMask(runtime.spaceHandle().value(), + boundRow.bodyHandle().value())); + BackendBodyHandle newlyBoundHandle = store + .getResource(PhysicsRuntimeResource.getResourceType()) + .getBodyHandle(unboundRow.ref()); + assertNotNull(newlyBoundHandle); + assertEquals(expectedMaterial.getFriction(), + runtime.backendRuntime().bodyFriction(runtime.spaceHandle().value(), + newlyBoundHandle.value()), + DELTA); + assertEquals(expectedMaterial.getRestitution(), + runtime.backendRuntime().bodyRestitution(runtime.spaceHandle().value(), + newlyBoundHandle.value()), + DELTA); + assertEquals(expectedFilter.getCollisionGroup(), + runtime.backendRuntime().bodyCollisionGroup(runtime.spaceHandle().value(), + newlyBoundHandle.value())); + assertEquals(expectedFilter.getCollisionMask(), + runtime.backendRuntime().bodyCollisionMask(runtime.spaceHandle().value(), + newlyBoundHandle.value())); + assertSoftSkipsEmpty(store); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Nonnull + private static RuntimeFixture addBoundSpace(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull BackendId backendId) { + Ref spaceRef = store.addEntity(PhysicsEntities.spaceHolder(store, + spaceUuid, + new SpaceComponent(backendId, new Vector3f(0.0f, -9.81f, 0.0f))), + AddReason.SPAWN); + assertNotNull(spaceRef); + store.getExternalData().putRefForUUID(spaceUuid, spaceRef); + + FakePhysicsBackendRuntime runtime = (FakePhysicsBackendRuntime) + new FakePhysicsBackendRuntimeProvider(backendId, false, false).createRuntime(); + BackendSpaceHandle spaceHandle = new BackendSpaceHandle(runtime.createSpace(new SpaceId(42))); + PhysicsRuntimeResource runtimeResource = store.getResource( + PhysicsRuntimeResource.getResourceType()); + runtimeResource.putRuntime(backendId, runtime); + runtimeResource.putSpaceHandle(spaceRef, backendId, spaceHandle); + runtimeResource.putSpaceMetadata(backendId, spaceHandle, spaceUuid, spaceRef); + return new RuntimeFixture(spaceUuid, backendId, spaceRef, spaceHandle, runtime); + } + + private static void putSpaceSurface(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull MaterialComponent material, + @Nonnull CollisionFilterComponent filter) { + store.putComponent(spaceRef, MaterialComponent.getComponentType(), material); + store.putComponent(spaceRef, CollisionFilterComponent.getComponentType(), filter); + } + + @Nonnull + private static GeneratedRow addGeneratedBoxRow(@Nonnull Store store, + @Nonnull RuntimeFixture runtime, + @Nonnull UUID spaceUuid, + @Nonnull String sourceKey, + boolean bindRuntime) { + UUID bodyUuid = ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + sourceKey, + PartKind.BOX, + 0); + BodyComponent body = new BodyComponent(spaceUuid); + body.setSpaceRef(runtime.spaceRef()); + Holder holder = PhysicsEntities.bodyHolder(store, + bodyUuid, + body, + new DynamicsComponent(PhysicsBodyType.STATIC, 0.0f, 0.0f, 0.0f, false), + target(), + new ColliderComponent(new Vector3f(), new Quaternionf(), false), + new ShapeComponent(ShapeType.BOX, + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + PhysicsAxis.Y, + 0.0f, + ""), + new MaterialComponent(0.11f, 0.01f), + new CollisionFilterComponent(0x01, 0x02)); + holder.addComponent(ChunkCollisionSourceComponent.getComponentType(), + new ChunkCollisionSourceComponent(sourceKey, 0, 0, 0, "", PartKind.BOX, 0)); + Ref bodyRef = store.addEntity(holder, AddReason.SPAWN); + assertNotNull(bodyRef); + + store.getExternalData().putRefForUUID(bodyUuid, bodyRef); + + if (!bindRuntime) { + return new GeneratedRow(bodyRef, new BackendBodyHandle(Long.MIN_VALUE)); + } + long bodyHandle = runtime.backendRuntime() + .createBody(runtime.spaceHandle().value(), + BackendRuntimeCodes.shapeTypeCode(ShapeType.BOX), + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + BackendRuntimeCodes.axisCode(PhysicsAxis.Y), + 0.0f, + 0.0f, + BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.STATIC), + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + runtime.backendRuntime().setBodyFriction(runtime.spaceHandle().value(), bodyHandle, 0.11f); + runtime.backendRuntime().setBodyRestitution(runtime.spaceHandle().value(), bodyHandle, 0.01f); + runtime.backendRuntime().setBodyCollisionFilter(runtime.spaceHandle().value(), + bodyHandle, + 0x01, + 0x02); + BackendBodyHandle backendBodyHandle = new BackendBodyHandle(bodyHandle); + PhysicsRuntimeResource runtimeResource = + store.getResource(PhysicsRuntimeResource.getResourceType()); + runtimeResource.putBodyHandle(bodyRef, + runtime.spaceRef(), + runtime.spaceHandle(), + backendBodyHandle); + runtimeResource.putBodySnapshotMetadata(runtime.backendId(), + runtime.spaceHandle(), + backendBodyHandle, + bodyUuid, + bodyRef, + runtime.spaceUuid()); + runtimeResource.putBodyHitMetadata(runtime.backendId(), + runtime.spaceHandle(), + backendBodyHandle, + bodyUuid, + bodyRef, + PhysicsBodyType.STATIC, + ShapeType.BOX); + return new GeneratedRow(bodyRef, backendBodyHandle); + } + + @Nonnull + private static TargetComponent target() { + TargetComponent target = new TargetComponent(); + target.setPosition(new Vector3f(1.0f, 2.0f, 3.0f)); + return target; + } + + private static void assertSurface(@Nonnull Store store, + @Nonnull Ref bodyRef, + @Nonnull MaterialComponent expectedMaterial, + @Nonnull CollisionFilterComponent expectedFilter) { + MaterialComponent material = store.getComponent(bodyRef, + MaterialComponent.getComponentType()); + assertNotNull(material); + assertEquals(expectedMaterial.getFriction(), material.getFriction(), DELTA); + assertEquals(expectedMaterial.getRestitution(), material.getRestitution(), DELTA); + CollisionFilterComponent filter = store.getComponent(bodyRef, + CollisionFilterComponent.getComponentType()); + assertNotNull(filter); + assertEquals(expectedFilter.getCollisionGroup(), filter.getCollisionGroup()); + assertEquals(expectedFilter.getCollisionMask(), filter.getCollisionMask()); + } + + private static void assertSoftSkipsEmpty(@Nonnull Store store) { + assertEquals(0, + store.getResource(PhysicsRestoreStatusResource.getResourceType()) + .getSoftSkipsByReason() + .size()); + } + + @Nonnull + private static UUID uuid(long leastSignificantBits) { + return new UUID(0L, leastSignificantBits); + } + + private record RuntimeFixture(@Nonnull UUID spaceUuid, + @Nonnull BackendId backendId, + @Nonnull Ref spaceRef, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull FakePhysicsBackendRuntime backendRuntime) { + } + + private record GeneratedRow(@Nonnull Ref ref, + @Nonnull BackendBodyHandle bodyHandle) { + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionMutationDrainSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionMutationDrainSystemTest.java new file mode 100644 index 00000000..07107d6f --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionMutationDrainSystemTest.java @@ -0,0 +1,1106 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.util.ChunkUtil; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.thread.TickingThread; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionMutation; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionPayload; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionPayload.BoxPayload; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkLifecycle; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkCollisionDefaults; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkStoreTypes; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsTopologyMutations; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionMutationQueueResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionPayloadResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkSettingsIndexResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkSettingsIndexResource.PhysicsChunkSpaceSettings; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent.PartKind; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.EntityChunkBoundaryMode; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsSnapshotFrame; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class ChunkCollisionMutationDrainSystemTest { + + private static final float DELTA = 0.000001f; + + @Test + void upsertCreatesGeneratedRowsWithReusableMaterialAndFilterComponents() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + registerTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("chunk-collision-drain-row-test")), + EmptyResourceStorage.get()); + try { + UUID spaceUuid = uuid(1); + BackendId backendId = new BackendId("test:chunk-collision-drain"); + Ref spaceRef = addBoundSpace(store, spaceUuid, backendId); + putChunkCollisionMaterialAndFilter(store, spaceRef, 0.82f, 0.18f, 0x40, 0x07); + String sourceKey = "0:1:2"; + String payloadKey = "chunk-collision/0/1/2"; + BoxPayload fullCubeBox = new BoxPayload(10.0, 20.0, 30.0, 1.5, 2.5, 3.5); + BoxPayload detailBox = new BoxPayload(40.0, 50.0, 60.0, 0.25, 0.5, 0.75); + ChunkCollisionPayload payload = new ChunkCollisionPayload(1.0f, + 1.0f, + 1.0f, + new int[0], + List.of(fullCubeBox), + List.of(detailBox), + false, + List.of()); + + PhysicsChunkCollisionMutationQueueResource queue = store.getResource( + PhysicsChunkCollisionMutationQueueResource.getResourceType()); + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + 0, + 1, + 2, + payloadKey, + payload)); + + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + assertEquals(0, queue.size()); + assertNull(store.getResource(PhysicsChunkCollisionPayloadResource.getResourceType()) + .get(payloadKey)); + assertSoftSkipsEmpty(store); + assertGeneratedBox(store, + spaceUuid, + spaceRef, + sourceKey, + payloadKey, + PartKind.BOX, + 0, + fullCubeBox); + assertGeneratedBox(store, + spaceUuid, + spaceRef, + sourceKey, + payloadKey, + PartKind.DETAIL_BOX, + 0, + detailBox); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void upsertCreatesNativeVoxelRowWhenBackendSupportsVoxelTerrain() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + registerTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("chunk-collision-drain-voxel-test")), + EmptyResourceStorage.get()); + try { + UUID spaceUuid = uuid(11); + BackendId backendId = new BackendId("test:chunk-collision-drain-voxel"); + Ref spaceRef = addBoundSpace(store, spaceUuid, backendId, true); + putChunkCollisionMaterialAndFilter(store, spaceRef, 0.7f, 0.05f, 0x20, 0x03); + String sourceKey = "2:3:4"; + String payloadKey = "chunk-collision/2/3/4"; + ChunkCollisionPayload payload = new ChunkCollisionPayload(1.0f, + 1.0f, + 1.0f, + new int[] {0, 0, 0, 1, 0, 0}, + List.of(), + List.of(), + true, + List.of()); + + PhysicsChunkCollisionMutationQueueResource queue = store.getResource( + PhysicsChunkCollisionMutationQueueResource.getResourceType()); + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + 2, + 3, + 4, + payloadKey, + payload)); + + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + assertEquals(0, queue.size()); + assertVoxelOnlyPayload(store.getResource(PhysicsChunkCollisionPayloadResource.getResourceType()) + .get(payloadKey)); + assertSoftSkipsEmpty(store); + assertGeneratedVoxel(store, + spaceUuid, + spaceRef, + sourceKey, + payloadKey); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void destroyingDetailRowDoesNotRemoveNativeVoxelPayloadForSiblingRow() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + registerTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("chunk-collision-drain-payload-lifetime-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(31); + BackendId backendId = new BackendId("test:chunk-collision-drain-payload-lifetime"); + Ref spaceRef = addBoundSpace(store, spaceUuid, backendId, true); + String sourceKey = "2:3:4"; + String payloadKey = "chunk-collision/2/3/4"; + ChunkCollisionPayload payload = new ChunkCollisionPayload(1.0f, + 1.0f, + 1.0f, + new int[] {0, 0, 0}, + List.of(), + List.of(new BoxPayload(10.0, 20.0, 30.0, 0.25, 0.5, 0.75)), + true, + List.of()); + + PhysicsChunkCollisionMutationQueueResource queue = store.getResource( + PhysicsChunkCollisionMutationQueueResource.getResourceType()); + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + 2, + 3, + 4, + payloadKey, + payload)); + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + assertGeneratedVoxel(store, spaceUuid, spaceRef, sourceKey, payloadKey); + UUID detailUuid = ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + sourceKey, + PartKind.DETAIL_BOX, + 0); + assertNotNull(store.getExternalData().getRefFromUUID(detailUuid)); + ChunkCollisionPayload retainedPayload = store.getResource( + PhysicsChunkCollisionPayloadResource.getResourceType()) + .get(payloadKey); + assertVoxelOnlyPayload(retainedPayload); + + PhysicsTopologyMutations.destroyBody(store, detailUuid); + + assertSame(retainedPayload, + store.getResource(PhysicsChunkCollisionPayloadResource.getResourceType()) + .get(payloadKey)); + assertSoftSkipsEmpty(store); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + private static void assertVoxelOnlyPayload(@Nullable ChunkCollisionPayload payload) { + assertNotNull(payload); + assertTrue(payload.hasFullCubeVoxels()); + assertTrue(payload.mergedFullCubeBoxes().isEmpty()); + assertTrue(payload.detailBoxes().isEmpty()); + } + + @Test + void removeDeletesGeneratedRowsAndPayloadResource() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + registerTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("chunk-collision-drain-remove-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(21); + BackendId backendId = new BackendId("test:chunk-collision-drain-remove"); + addBoundSpace(store, spaceUuid, backendId); + String sourceKey = "5:6:7"; + String payloadKey = "chunk-collision/5/6/7"; + ChunkCollisionPayload payload = new ChunkCollisionPayload(1.0f, + 1.0f, + 1.0f, + new int[0], + List.of(new BoxPayload(1.0, 2.0, 3.0, 0.5, 0.5, 0.5)), + List.of(new BoxPayload(4.0, 5.0, 6.0, 0.25, 0.25, 0.25)), + false, + List.of()); + + PhysicsChunkCollisionMutationQueueResource queue = store.getResource( + PhysicsChunkCollisionMutationQueueResource.getResourceType()); + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + 5, + 6, + 7, + payloadKey, + payload)); + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + UUID boxUuid = ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + sourceKey, + PartKind.BOX, + 0); + UUID detailUuid = ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + sourceKey, + PartKind.DETAIL_BOX, + 0); + Ref boxRef = store.getExternalData().getRefFromUUID(boxUuid); + Ref detailRef = store.getExternalData().getRefFromUUID(detailUuid); + assertNotNull(boxRef); + assertNotNull(detailRef); + publishCopiedState(store, spaceUuid, boxUuid, boxRef, detailUuid, detailRef); + PhysicsSnapshotResource snapshots = + store.getResource(PhysicsSnapshotResource.getResourceType()); + assertNotNull(snapshots.getBody(boxUuid)); + assertNotNull(snapshots.getBody(detailUuid)); + assertTrue(PhysicsBodies.isRegistered(store, boxUuid)); + assertTrue(PhysicsBodies.isRegistered(store, detailUuid)); + + queue.enqueue(ChunkCollisionMutation.remove(spaceUuid, sourceKey, 5, 6, 7)); + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + assertEquals(0, queue.size()); + assertNull(store.getExternalData().getRefFromUUID(boxUuid)); + assertNull(store.getExternalData().getRefFromUUID(detailUuid)); + assertNull(snapshots.getBody(boxUuid)); + assertNull(snapshots.getBody(detailUuid)); + assertFalse(PhysicsBodies.isRegistered(store, boxUuid)); + assertFalse(PhysicsBodies.isRegistered(store, detailUuid)); + assertNull(store.getResource(PhysicsChunkCollisionPayloadResource.getResourceType()) + .get(payloadKey)); + assertSoftSkipsEmpty(store); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void removeDeletesManyGeneratedSourcesWithoutPerSourceFullStoreScans() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + registerTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("chunk-collision-drain-remove-scale-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(71); + addBoundSpace(store, + spaceUuid, + new BackendId("test:chunk-collision-drain-remove-scale")); + PhysicsChunkCollisionMutationQueueResource queue = store.getResource( + PhysicsChunkCollisionMutationQueueResource.getResourceType()); + int sources = 4_096; + for (int index = 0; index < sources; index++) { + String sourceKey = "scale:" + index; + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + index, + 1, + 2, + "chunk-collision/scale/" + index, + boxPayload(index, 2.0, 3.0))); + } + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + assertNotNull(store.getExternalData() + .getRefFromUUID(ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + "scale:0", + PartKind.BOX, + 0))); + assertNotNull(store.getExternalData() + .getRefFromUUID(ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + "scale:" + (sources - 1), + PartKind.BOX, + 0))); + + for (int index = 0; index < sources; index++) { + queue.enqueue(ChunkCollisionMutation.remove(spaceUuid, + "scale:" + index, + index, + 1, + 2)); + } + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + assertEquals(0, queue.size()); + for (int index = 0; index < sources; index++) { + assertNull(store.getExternalData() + .getRefFromUUID(ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid( + spaceUuid, + "scale:" + index, + PartKind.BOX, + 0))); + } + assertSoftSkipsEmpty(store); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void invalidFreshReplacementKeepsExistingGeneratedRows() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + registerTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("chunk-collision-drain-invalid-replace-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(72); + Ref spaceRef = addBoundSpace(store, + spaceUuid, + new BackendId("test:chunk-collision-drain-invalid-replace")); + String sourceKey = "invalid:replace"; + String payloadKey = "chunk-collision/invalid/replace"; + BoxPayload initialBox = new BoxPayload(1.0, 2.0, 3.0, 0.5, 0.5, 0.5); + ChunkCollisionPayload initialPayload = new ChunkCollisionPayload(1.0f, + 1.0f, + 1.0f, + new int[0], + List.of(initialBox), + List.of(), + false, + List.of()); + + PhysicsChunkCollisionMutationQueueResource queue = store.getResource( + PhysicsChunkCollisionMutationQueueResource.getResourceType()); + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + 0, + 1, + 2, + payloadKey, + initialPayload)); + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + ChunkCollisionPayload emptyPayload = new ChunkCollisionPayload(1.0f, + 1.0f, + 1.0f, + new int[0], + List.of(), + List.of(), + false, + List.of()); + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + 0, + 1, + 2, + payloadKey, + emptyPayload)); + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + assertEquals(0, queue.size()); + assertGeneratedBox(store, + spaceUuid, + spaceRef, + sourceKey, + payloadKey, + PartKind.BOX, + 0, + initialBox); + assertSoftSkip(store, + "Chunk collision upsert payload is missing: " + sourceKey, + 1); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void unboundFreshReplacementKeepsExistingGeneratedRows() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + registerTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("chunk-collision-drain-unbound-replace-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(73); + Ref spaceRef = addBoundSpace(store, + spaceUuid, + new BackendId("test:chunk-collision-drain-unbound-replace")); + String sourceKey = "unbound:replace"; + String payloadKey = "chunk-collision/unbound/replace"; + BoxPayload initialBox = new BoxPayload(1.0, 2.0, 3.0, 0.5, 0.5, 0.5); + + PhysicsChunkCollisionMutationQueueResource queue = store.getResource( + PhysicsChunkCollisionMutationQueueResource.getResourceType()); + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + 0, + 1, + 2, + payloadKey, + new ChunkCollisionPayload(1.0f, + 1.0f, + 1.0f, + new int[0], + List.of(initialBox), + List.of(), + false, + List.of()))); + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + store.getResource(PhysicsRuntimeResource.getResourceType()) + .removeSpaceHandle(spaceRef); + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + 0, + 1, + 2, + payloadKey, + boxPayload(4.0, 5.0, 6.0))); + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + assertEquals(0, queue.size()); + assertGeneratedBox(store, + spaceUuid, + spaceRef, + sourceKey, + payloadKey, + PartKind.BOX, + 0, + initialBox); + assertSoftSkip(store, + "Chunk collision references unbound space: " + sourceKey, + 1); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void staleLifecycleUpsertDoesNotCreateGeneratedRows() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + registerTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("chunk-collision-drain-stale-lifecycle-test")), + EmptyResourceStorage.get()); + try { + UUID spaceUuid = uuid(51); + addBoundSpace(store, spaceUuid, new BackendId("test:chunk-collision-stale-lifecycle")); + String sourceKey = "1:2:3"; + String payloadKey = "chunk-collision/1/2/3"; + PhysicsChunkCollisionMutationQueueResource queue = store.getResource( + PhysicsChunkCollisionMutationQueueResource.getResourceType()); + queue.updateStamp(previousGeneration(PhysicsChunkLifecycle.generation()), + settingsGeneration(store)); + + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + 1, + 2, + 3, + payloadKey, + boxPayload(1.0, 2.0, 3.0))); + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + assertEquals(0, queue.size()); + assertNull(store.getExternalData() + .getRefFromUUID(ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + sourceKey, + PartKind.BOX, + 0))); + assertNull(store.getResource(PhysicsChunkCollisionPayloadResource.getResourceType()) + .get(payloadKey)); + assertSoftSkipsEmpty(store); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void staleSettingsUpsertDoesNotCreateGeneratedRows() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + registerTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("chunk-collision-drain-stale-settings-test")), + EmptyResourceStorage.get()); + try { + UUID spaceUuid = uuid(52); + addBoundSpace(store, spaceUuid, new BackendId("test:chunk-collision-stale-settings")); + String sourceKey = "2:3:4"; + String payloadKey = "chunk-collision/2/3/4"; + PhysicsChunkCollisionMutationQueueResource queue = store.getResource( + PhysicsChunkCollisionMutationQueueResource.getResourceType()); + queue.updateStamp(PhysicsChunkLifecycle.generation(), + previousGeneration(settingsGeneration(store))); + + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + 2, + 3, + 4, + payloadKey, + boxPayload(4.0, 5.0, 6.0))); + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + assertEquals(0, queue.size()); + assertNull(store.getExternalData() + .getRefFromUUID(ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + sourceKey, + PartKind.BOX, + 0))); + assertNull(store.getResource(PhysicsChunkCollisionPayloadResource.getResourceType()) + .get(payloadKey)); + assertSoftSkipsEmpty(store); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void staleRemoveStillDeletesGeneratedRows() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + registerTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("chunk-collision-drain-stale-remove-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(53); + addBoundSpace(store, spaceUuid, new BackendId("test:chunk-collision-stale-remove")); + String sourceKey = "3:4:5"; + String payloadKey = "chunk-collision/3/4/5"; + PhysicsChunkCollisionMutationQueueResource queue = store.getResource( + PhysicsChunkCollisionMutationQueueResource.getResourceType()); + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + 3, + 4, + 5, + payloadKey, + boxPayload(7.0, 8.0, 9.0))); + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + assertNotNull(store.getExternalData() + .getRefFromUUID(ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + sourceKey, + PartKind.BOX, + 0))); + + queue.updateStamp(previousGeneration(PhysicsChunkLifecycle.generation()), + previousGeneration(settingsGeneration(store))); + queue.enqueue(ChunkCollisionMutation.remove(spaceUuid, sourceKey, 3, 4, 5)); + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + assertEquals(0, queue.size()); + assertNull(store.getExternalData() + .getRefFromUUID(ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + sourceKey, + PartKind.BOX, + 0))); + assertSoftSkipsEmpty(store); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void sameDrainUpsertThenRemoveKeepsRemoveIntent() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + registerTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("chunk-collision-drain-upsert-remove-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(41); + BackendId backendId = new BackendId("test:chunk-collision-drain-upsert-remove"); + addBoundSpace(store, spaceUuid, backendId); + String sourceKey = "8:9:10"; + String payloadKey = "chunk-collision/8/9/10"; + ChunkCollisionPayload payload = boxPayload(1.0, 2.0, 3.0); + + PhysicsChunkCollisionMutationQueueResource queue = store.getResource( + PhysicsChunkCollisionMutationQueueResource.getResourceType()); + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + 8, + 9, + 10, + payloadKey, + payload)); + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + UUID boxUuid = ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + sourceKey, + PartKind.BOX, + 0); + assertNotNull(store.getExternalData().getRefFromUUID(boxUuid)); + + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + 8, + 9, + 10, + payloadKey, + boxPayload(4.0, 5.0, 6.0))); + queue.enqueue(ChunkCollisionMutation.remove(spaceUuid, sourceKey, 8, 9, 10)); + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + assertEquals(0, queue.size()); + assertNull(store.getExternalData().getRefFromUUID(boxUuid)); + assertSoftSkipsEmpty(store); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void sameDrainRemoveThenUpsertKeepsUpsertIntent() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + registerTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("chunk-collision-drain-remove-upsert-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(42); + BackendId backendId = new BackendId("test:chunk-collision-drain-remove-upsert"); + Ref spaceRef = addBoundSpace(store, spaceUuid, backendId); + String sourceKey = "0:1:2"; + String payloadKey = "chunk-collision/0/1/2"; + + PhysicsChunkCollisionMutationQueueResource queue = store.getResource( + PhysicsChunkCollisionMutationQueueResource.getResourceType()); + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + 0, + 1, + 2, + payloadKey, + boxPayload(1.0, 2.0, 3.0))); + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + BoxPayload updatedBox = new BoxPayload(4.0, 5.0, 6.0, 0.5, 0.5, 0.5); + ChunkCollisionPayload updatedPayload = new ChunkCollisionPayload(1.0f, + 1.0f, + 1.0f, + new int[0], + List.of(updatedBox), + List.of(), + false, + List.of()); + queue.enqueue(ChunkCollisionMutation.remove(spaceUuid, sourceKey, 0, 1, 2)); + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + sourceKey, + 0, + 1, + 2, + payloadKey, + updatedPayload)); + new ChunkCollisionMutationDrainSystem().tick(0.0f, 0, store); + + assertEquals(0, queue.size()); + assertGeneratedBox(store, + spaceUuid, + spaceRef, + sourceKey, + payloadKey, + PartKind.BOX, + 0, + updatedBox); + assertSoftSkipsEmpty(store); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Nonnull + private static Ref addBoundSpace(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull BackendId backendId) { + return addBoundSpace(store, spaceUuid, backendId, false); + } + + private static void registerTypes( + @Nonnull ComponentRegistryProxy proxy) { + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + PhysicsChunkStoreTypes.registerPhysicsStoreResourceTypes(proxy); + } + + @Nonnull + private static Ref addBoundSpace(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull BackendId backendId, + boolean voxelTerrain) { + Ref spaceRef = store.addEntity(PhysicsEntities.spaceHolder(store, + spaceUuid, + new SpaceComponent(backendId, new Vector3f(0.0f, -9.81f, 0.0f))), + AddReason.SPAWN); + assertNotNull(spaceRef); + store.getExternalData().putRefForUUID(spaceUuid, spaceRef); + + PhysicsBackendRuntime backendRuntime = + new FakePhysicsBackendRuntimeProvider(backendId, false, voxelTerrain).createRuntime(); + int spaceHandle = backendRuntime.createSpace(new SpaceId(42)); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + BackendSpaceHandle backendSpaceHandle = new BackendSpaceHandle(spaceHandle); + runtime.putRuntime(backendId, backendRuntime); + runtime.putSpaceHandle(spaceRef, backendId, backendSpaceHandle); + runtime.putSpaceMetadata(backendId, backendSpaceHandle, spaceUuid, spaceRef); + store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .putSpace(new SpaceId(42), spaceUuid); + publishSettingsIndex(store, spaceUuid); + return spaceRef; + } + + private static void publishSettingsIndex(@Nonnull Store store, + @Nonnull UUID spaceUuid) { + PhysicsChunkSettingsIndexResource settingsIndex = + store.getResource(PhysicsChunkSettingsIndexResource.getResourceType()); + settingsIndex.replaceAll(Map.of(spaceUuid, new PhysicsChunkSpaceSettings(spaceUuid, + PhysicsChunkCollisionMode.MANUAL, + EntityChunkBoundaryMode.PAUSE_UNTIL_LOADED, + false, + 1, + 1, + 20))); + store.getResource(PhysicsChunkCollisionMutationQueueResource.getResourceType()) + .updateStamp(PhysicsChunkLifecycle.generation(), settingsIndex.generation()); + } + + private static long settingsGeneration(@Nonnull Store store) { + return store.getResource(PhysicsChunkSettingsIndexResource.getResourceType()) + .generation(); + } + + private static void publishCopiedState(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull UUID firstBodyUuid, + @Nonnull Ref firstBodyRef, + @Nonnull UUID secondBodyUuid, + @Nonnull Ref secondBodyRef) { + store.getResource(PhysicsSnapshotResource.getResourceType()) + .publish(new PhysicsSnapshotFrame(1L, + 0.05f, + List.of(snapshot(firstBodyRef, firstBodyUuid, spaceUuid), + snapshot(secondBodyRef, secondBodyUuid, spaceUuid)))); + } + + @Nonnull + private static PhysicsBodySnapshot snapshot(@Nonnull Ref bodyRef, + @Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid) { + return PhysicsBodySnapshot.of(bodyRef, + bodyUuid, + spaceUuid, + PhysicsBodyType.STATIC, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + false); + } + + private static long previousGeneration(long generation) { + return Math.max(0L, generation - 1L); + } + + @Nonnull + private static ChunkCollisionPayload boxPayload(double centerX, + double centerY, + double centerZ) { + return new ChunkCollisionPayload(1.0f, + 1.0f, + 1.0f, + new int[0], + List.of(new BoxPayload(centerX, centerY, centerZ, 0.5, 0.5, 0.5)), + List.of(), + false, + List.of()); + } + + private static void putChunkCollisionMaterialAndFilter(@Nonnull Store store, + @Nonnull Ref spaceRef, + float friction, + float restitution, + int collisionGroup, + int collisionMask) { + store.putComponent(spaceRef, + MaterialComponent.getComponentType(), + new MaterialComponent(friction, restitution)); + store.putComponent(spaceRef, + CollisionFilterComponent.getComponentType(), + new CollisionFilterComponent(collisionGroup, collisionMask)); + } + + private static void assertGeneratedVoxel(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull Ref spaceRef, + @Nonnull String sourceKey, + @Nonnull String payloadKey) { + UUID bodyUuid = ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + sourceKey, + PartKind.NATIVE_VOXELS, + 0); + Ref bodyRef = store.getExternalData().getRefFromUUID(bodyUuid); + assertNotNull(bodyRef); + + BodyComponent body = store.getComponent(bodyRef, BodyComponent.getComponentType()); + assertNotNull(body); + assertEquals(spaceUuid, body.getSpaceUuid()); + assertNotNull(body.getSpaceRef()); + assertEquals(spaceRef.getIndex(), body.getSpaceRef().getIndex()); + + ShapeComponent shape = store.getComponent(bodyRef, ShapeComponent.getComponentType()); + assertNotNull(shape); + assertEquals(ShapeType.VOXELS, shape.getShapeType()); + assertEquals(payloadKey, shape.getResourceKey()); + + TargetComponent target = store.getComponent(bodyRef, TargetComponent.getComponentType()); + assertNotNull(target); + assertVectorEquals(2 << ChunkUtil.BITS, + 3 << ChunkUtil.BITS, + 4 << ChunkUtil.BITS, + target.getPosition()); + + MaterialComponent material = store.getComponent(bodyRef, + MaterialComponent.getComponentType()); + assertNotNull(material); + assertMaterialMatchesSpace(store, spaceRef, material); + + CollisionFilterComponent filter = store.getComponent(bodyRef, + CollisionFilterComponent.getComponentType()); + assertNotNull(filter); + assertFilterMatchesSpace(store, spaceRef, filter); + + ChunkCollisionSourceComponent source = store.getComponent(bodyRef, + ChunkCollisionSourceComponent.getComponentType()); + assertNotNull(source); + assertEquals(sourceKey, source.getSourceKey()); + assertEquals(payloadKey, source.getPayloadResourceKey()); + assertEquals(PartKind.NATIVE_VOXELS, source.getPartKind()); + assertEquals(0, source.getPartIndex()); + } + + private static void assertGeneratedBox(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull Ref spaceRef, + @Nonnull String sourceKey, + @Nonnull String payloadKey, + @Nonnull PartKind partKind, + int partIndex, + @Nonnull BoxPayload box) { + UUID bodyUuid = ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + sourceKey, + partKind, + partIndex); + Ref bodyRef = store.getExternalData().getRefFromUUID(bodyUuid); + assertNotNull(bodyRef); + assertEquals(bodyUuid, + store.getComponent(bodyRef, UuidComponent.getComponentType()).getUuid()); + + BodyComponent body = store.getComponent(bodyRef, BodyComponent.getComponentType()); + assertNotNull(body); + assertEquals(spaceUuid, body.getSpaceUuid()); + assertNotNull(body.getSpaceRef()); + assertEquals(spaceRef.getIndex(), body.getSpaceRef().getIndex()); + + DynamicsComponent dynamics = store.getComponent(bodyRef, + DynamicsComponent.getComponentType()); + assertNotNull(dynamics); + assertEquals(PhysicsBodyType.STATIC, dynamics.getBodyType()); + assertEquals(0.0f, dynamics.getMass(), DELTA); + assertFalse(dynamics.isContinuousCollisionEnabled()); + + TargetComponent target = store.getComponent(bodyRef, TargetComponent.getComponentType()); + assertNotNull(target); + assertVectorEquals((float) box.centerX(), + (float) box.centerY(), + (float) box.centerZ(), + target.getPosition()); + + ShapeComponent shape = store.getComponent(bodyRef, ShapeComponent.getComponentType()); + assertNotNull(shape); + assertEquals(ShapeType.BOX, shape.getShapeType()); + assertEquals((float) box.halfX(), shape.getHalfExtentX(), DELTA); + assertEquals((float) box.halfY(), shape.getHalfExtentY(), DELTA); + assertEquals((float) box.halfZ(), shape.getHalfExtentZ(), DELTA); + assertEquals("", shape.getResourceKey()); + + MaterialComponent material = store.getComponent(bodyRef, + MaterialComponent.getComponentType()); + assertNotNull(material); + assertMaterialMatchesSpace(store, spaceRef, material); + + CollisionFilterComponent filter = store.getComponent(bodyRef, + CollisionFilterComponent.getComponentType()); + assertNotNull(filter); + assertFilterMatchesSpace(store, spaceRef, filter); + + ChunkCollisionSourceComponent source = store.getComponent(bodyRef, + ChunkCollisionSourceComponent.getComponentType()); + assertNotNull(source); + assertEquals(sourceKey, source.getSourceKey()); + assertEquals(0, source.getChunkX()); + assertEquals(1, source.getSectionY()); + assertEquals(2, source.getChunkZ()); + assertEquals("", source.getPayloadResourceKey()); + assertEquals(partKind, source.getPartKind()); + assertEquals(partIndex, source.getPartIndex()); + } + + private static void assertMaterialMatchesSpace(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull MaterialComponent material) { + MaterialComponent spaceMaterial = store.getComponent(spaceRef, + MaterialComponent.getComponentType()); + float expectedFriction = spaceMaterial != null + ? spaceMaterial.getFriction() + : PhysicsChunkCollisionDefaults.FRICTION; + float expectedRestitution = spaceMaterial != null + ? spaceMaterial.getRestitution() + : PhysicsChunkCollisionDefaults.RESTITUTION; + assertEquals(expectedFriction, material.getFriction(), DELTA); + assertEquals(expectedRestitution, material.getRestitution(), DELTA); + } + + private static void assertFilterMatchesSpace(@Nonnull Store store, + @Nonnull Ref spaceRef, + @Nonnull CollisionFilterComponent filter) { + CollisionFilterComponent spaceFilter = store.getComponent(spaceRef, + CollisionFilterComponent.getComponentType()); + int expectedGroup = spaceFilter != null + ? spaceFilter.getCollisionGroup() + : PhysicsCollisionFilters.TERRAIN; + int expectedMask = spaceFilter != null + ? spaceFilter.getCollisionMask() + : PhysicsCollisionFilters.ALL; + assertEquals(expectedGroup, filter.getCollisionGroup()); + assertEquals(expectedMask, filter.getCollisionMask()); + } + + private static void assertVectorEquals(float expectedX, + float expectedY, + float expectedZ, + @Nonnull Vector3f actual) { + assertEquals(expectedX, actual.x, DELTA); + assertEquals(expectedY, actual.y, DELTA); + assertEquals(expectedZ, actual.z, DELTA); + } + + private static void assertSoftSkipsEmpty(@Nonnull Store store) { + assertEquals(0, + store.getResource(PhysicsRestoreStatusResource.getResourceType()) + .getSoftSkipsByReason() + .size()); + } + + private static void assertSoftSkip(@Nonnull Store store, + @Nonnull String reason, + int count) { + var softSkips = store.getResource(PhysicsRestoreStatusResource.getResourceType()) + .getSoftSkipsByReason(); + assertEquals(1, softSkips.size()); + assertEquals(count, softSkips.getInt(reason)); + } + + private static void markCurrentThreadAsWorldThread(@Nonnull Store store) { + try { + Method setThread = TickingThread.class.getDeclaredMethod("setThread", Thread.class); + setThread.setAccessible(true); + setThread.invoke(store.getExternalData().getWorld(), Thread.currentThread()); + } catch (NoSuchMethodException | IllegalAccessException exception) { + throw new AssertionError("Could not mark test world thread", exception); + } catch (InvocationTargetException exception) { + throw new AssertionError("Could not mark test world thread", exception.getCause()); + } + } + + @Nonnull + private static UUID uuid(long leastSignificantBits) { + return new UUID(0L, leastSignificantBits); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionVoxelStitchingSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionVoxelStitchingSystemTest.java new file mode 100644 index 00000000..5ac04c44 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicschunk/systems/ChunkCollisionVoxelStitchingSystemTest.java @@ -0,0 +1,379 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.PhysicsAxis; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.CombineCall; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.FakePhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionPayload; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionPayload.Neighbor; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkStoreTypes; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionPayloadResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent.PartKind; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class ChunkCollisionVoxelStitchingSystemTest { + + @Test + void voxelRowsAreStitchedThroughBodyRuntimeHandlesOncePerPayload() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + PhysicsChunkStoreTypes.registerPhysicsStoreResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("chunk-collision-stitch-row-test")), + EmptyResourceStorage.get()); + try { + BackendId backendId = new BackendId("test:chunk-collision-stitch"); + UUID spaceUuid = uuid(1); + RuntimeFixture runtime = addBoundSpace(store, spaceUuid, backendId); + String firstSourceKey = "0:0:0"; + String secondSourceKey = "1:0:0"; + String firstPayloadKey = "chunk-collision/0/0/0"; + String secondPayloadKey = "chunk-collision/1/0/0"; + Ref firstRef = addVoxelRow(store, + runtime, + spaceUuid, + firstSourceKey, + firstPayloadKey, + 0, + 0, + 0); + Ref secondRef = addVoxelRow(store, + runtime, + spaceUuid, + secondSourceKey, + secondPayloadKey, + 1, + 0, + 0); + long firstBodyId = store.getResource(PhysicsRuntimeResource.getResourceType()) + .getBodyHandle(firstRef) + .value(); + long secondBodyId = store.getResource(PhysicsRuntimeResource.getResourceType()) + .getBodyHandle(secondRef) + .value(); + store.getResource(PhysicsChunkCollisionPayloadResource.getResourceType()) + .put(firstPayloadKey, + payloadWithNeighbors(List.of(new Neighbor(secondSourceKey, 16, 0, 0)))); + store.getResource(PhysicsChunkCollisionPayloadResource.getResourceType()) + .put(secondPayloadKey, + payloadWithNeighbors(List.of(new Neighbor(firstSourceKey, -16, 0, 0)))); + + runStitchingSystem(store); + runStitchingSystem(store); + + List calls = runtime.backendRuntime().combineCalls(runtime.spaceHandle() + .value()); + assertEquals(1, calls.size()); + CombineCall call = calls.getFirst(); + assertEquals(firstBodyId, call.bodyAId()); + assertEquals(secondBodyId, call.bodyBId()); + assertEquals(16, call.shiftX()); + assertEquals(0, call.shiftY()); + assertEquals(0, call.shiftZ()); + assertSoftSkipsEmpty(store); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void voxelRowsWithSameNumericSpaceHandleAreNotStitchedAcrossBackends() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + PhysicsChunkStoreTypes.registerPhysicsStoreResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("chunk-collision-cross-backend-test")), + EmptyResourceStorage.get()); + try { + UUID spaceUuid = uuid(11); + RuntimeFixture joltRuntime = addBoundSpace(store, + spaceUuid, + new BackendId("test:chunk-collision-jolt")); + RuntimeFixture rapierRuntime = addBoundSpace(store, + uuid(12), + new BackendId("test:chunk-collision-rapier")); + String firstSourceKey = "0:0:0"; + String secondSourceKey = "1:0:0"; + String firstPayloadKey = "chunk-collision/cross-backend/0"; + String secondPayloadKey = "chunk-collision/cross-backend/1"; + addVoxelRow(store, + joltRuntime, + spaceUuid, + firstSourceKey, + firstPayloadKey, + 0, + 0, + 0); + addVoxelRow(store, + rapierRuntime, + spaceUuid, + secondSourceKey, + secondPayloadKey, + 1, + 0, + 0); + store.getResource(PhysicsChunkCollisionPayloadResource.getResourceType()) + .put(firstPayloadKey, + payloadWithNeighbors(List.of(new Neighbor(secondSourceKey, 16, 0, 0)))); + store.getResource(PhysicsChunkCollisionPayloadResource.getResourceType()) + .put(secondPayloadKey, payloadWithNeighbors(List.of())); + + runStitchingSystem(store); + + assertEquals(List.of(), + joltRuntime.backendRuntime().combineCalls(joltRuntime.spaceHandle().value())); + assertEquals(List.of(), + rapierRuntime.backendRuntime().combineCalls(rapierRuntime.spaceHandle().value())); + assertSoftSkipsEmpty(store); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Nonnull + private static RuntimeFixture addBoundSpace(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull BackendId backendId) { + Ref spaceRef = store.addEntity(PhysicsEntities.spaceHolder(store, + spaceUuid, + new SpaceComponent(backendId, new Vector3f(0.0f, -9.81f, 0.0f))), + AddReason.SPAWN); + assertNotNull(spaceRef); + store.getExternalData().putRefForUUID(spaceUuid, spaceRef); + + FakePhysicsBackendRuntime runtime = (FakePhysicsBackendRuntime) + new FakePhysicsBackendRuntimeProvider(backendId, false, true).createRuntime(); + BackendSpaceHandle spaceHandle = new BackendSpaceHandle(runtime.createSpace(new SpaceId(42))); + PhysicsRuntimeResource runtimeResource = store.getResource( + PhysicsRuntimeResource.getResourceType()); + runtimeResource.putRuntime(backendId, runtime); + runtimeResource.putSpaceHandle(spaceRef, backendId, spaceHandle); + runtimeResource.putSpaceMetadata(backendId, spaceHandle, spaceUuid, spaceRef); + return new RuntimeFixture(spaceUuid, backendId, spaceRef, spaceHandle, runtime); + } + + @Nonnull + private static Ref addVoxelRow(@Nonnull Store store, + @Nonnull RuntimeFixture runtime, + @Nonnull UUID spaceUuid, + @Nonnull String sourceKey, + @Nonnull String payloadKey, + int chunkX, + int sectionY, + int chunkZ) { + UUID bodyUuid = ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + sourceKey, + PartKind.NATIVE_VOXELS, + 0); + BodyComponent body = new BodyComponent(spaceUuid); + body.setSpaceRef(runtime.spaceRef()); + Holder holder = PhysicsEntities.bodyHolder(store, + bodyUuid, + body, + new DynamicsComponent(PhysicsBodyType.STATIC, 0.0f, 0.0f, 0.0f, false), + target(chunkX, sectionY, chunkZ), + new ColliderComponent(new Vector3f(), new Quaternionf(), false), + new ShapeComponent(ShapeType.VOXELS, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + PhysicsAxis.Y, + 0.0f, + payloadKey), + new MaterialComponent(0.7f, 0.05f), + new CollisionFilterComponent(PhysicsCollisionFilters.TERRAIN, + PhysicsCollisionFilters.ALL)); + holder.addComponent(ChunkCollisionSourceComponent.getComponentType(), + new ChunkCollisionSourceComponent(sourceKey, + chunkX, + sectionY, + chunkZ, + payloadKey, + PartKind.NATIVE_VOXELS, + 0)); + Ref bodyRef = store.addEntity(holder, AddReason.SPAWN); + assertNotNull(bodyRef); + store.getExternalData().putRefForUUID(bodyUuid, bodyRef); + + long bodyHandle = runtime.backendRuntime() + .createVoxelTerrain(runtime.spaceHandle().value(), + 1.0f, + 1.0f, + 1.0f, + new int[] {0, 0, 0}, + chunkX * 16.0f, + sectionY * 16.0f, + chunkZ * 16.0f, + 0.7f, + 0.05f, + PhysicsCollisionFilters.TERRAIN, + PhysicsCollisionFilters.ALL); + BackendBodyHandle backendBodyHandle = new BackendBodyHandle(bodyHandle); + PhysicsRuntimeResource runtimeResource = + store.getResource(PhysicsRuntimeResource.getResourceType()); + runtimeResource.putBodyHandle(bodyRef, + runtime.spaceRef(), + runtime.spaceHandle(), + backendBodyHandle); + runtimeResource.putBodySnapshotMetadata(runtime.backendId(), + runtime.spaceHandle(), + backendBodyHandle, + bodyUuid, + bodyRef, + runtime.spaceUuid()); + runtimeResource.putBodyHitMetadata(runtime.backendId(), + runtime.spaceHandle(), + backendBodyHandle, + bodyUuid, + bodyRef, + PhysicsBodyType.STATIC, + ShapeType.VOXELS); + return bodyRef; + } + + @Nonnull + private static TargetComponent target(int chunkX, int sectionY, int chunkZ) { + TargetComponent target = new TargetComponent(); + target.setPosition(new Vector3f(chunkX * 16.0f, sectionY * 16.0f, chunkZ * 16.0f)); + return target; + } + + @Nonnull + private static ChunkCollisionPayload payloadWithNeighbors( + @Nonnull List neighbors) { + return new ChunkCollisionPayload(1.0f, + 1.0f, + 1.0f, + new int[] {0, 0, 0}, + List.of(), + List.of(), + true, + neighbors); + } + + private static void runStitchingSystem(@Nonnull Store store) { + try { + ChunkCollisionVoxelStitchingSystem system = new ChunkCollisionVoxelStitchingSystem(); + Method stitchChunk = ChunkCollisionVoxelStitchingSystem.class.getDeclaredMethod( + "stitchChunk", + Store.class, + PhysicsRuntimeResource.class, + PhysicsChunkCollisionPayloadResource.class, + PhysicsRestoreStatusResource.class, + Set.class, + ArchetypeChunk.class); + stitchChunk.setAccessible(true); + Set stitchedPairs = new HashSet<>(); + PhysicsRuntimeResource runtime = store.getResource( + PhysicsRuntimeResource.getResourceType()); + PhysicsChunkCollisionPayloadResource payloads = store.getResource( + PhysicsChunkCollisionPayloadResource.getResourceType()); + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + BiConsumer, CommandBuffer> collector = + (chunk, _) -> invoke(stitchChunk, + store, + runtime, + payloads, + restore, + stitchedPairs, + chunk); + store.forEachChunk(system.getQuery(), collector); + } catch (NoSuchMethodException exception) { + throw new AssertionError("Could not run ChunkCollisionVoxelStitchingSystem", + exception); + } + } + + private static void invoke(@Nonnull Method method, @Nonnull Object... arguments) { + try { + method.invoke(null, arguments); + } catch (IllegalAccessException exception) { + throw new AssertionError(exception); + } catch (InvocationTargetException exception) { + Throwable cause = exception.getCause(); + if (cause instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (cause instanceof Error error) { + throw error; + } + throw new AssertionError(cause); + } + } + + private static void assertSoftSkipsEmpty(@Nonnull Store store) { + assertEquals(0, + store.getResource(PhysicsRestoreStatusResource.getResourceType()) + .getSoftSkipsByReason() + .size()); + } + + @Nonnull + private static UUID uuid(long leastSignificantBits) { + return new UUID(0L, leastSignificantBits); + } + + private record RuntimeFixture(@Nonnull UUID spaceUuid, + @Nonnull BackendId backendId, + @Nonnull Ref spaceRef, + @Nonnull BackendSpaceHandle spaceHandle, + @Nonnull FakePhysicsBackendRuntime backendRuntime) { + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityLifecycleTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityLifecycleTest.java new file mode 100644 index 00000000..2b9af863 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityLifecycleTest.java @@ -0,0 +1,60 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicsentity; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsEntityAttachments; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsEntityTypes; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.GeneratedVisualProxyComponent; +import java.util.ArrayList; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class PhysicsEntityLifecycleTest { + + @AfterEach + void clearLifecycleAndTypes() { + PhysicsEntityLifecycle.disable(); + PhysicsEntityTypeRegistry.clearEntityStoreTypes(); + } + + @Test + void attachmentsAreUnavailableWithoutLifecycleAndRegisteredTypes() { + assertFalse(PhysicsEntityLifecycle.isEnabled()); + assertFalse(PhysicsEntityTypes.areEntityStoreTypesRegistered()); + assertFalse(BodyAttachmentComponent.isComponentTypeRegistered()); + assertFalse(GeneratedVisualProxyComponent.isComponentTypeRegistered()); + assertFalse(PhysicsEntityAttachments.isAvailable()); + } + + @Test + void attachmentsAreAvailableOnlyWhenLifecycleAndTypesAreRegistered() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + try { + PhysicsEntityLifecycle.enable(); + assertFalse(PhysicsEntityAttachments.isAvailable()); + + PhysicsEntityTypeRegistry.registerComponentTypes(proxy); + PhysicsEntityTypeRegistry.registerResourceTypes(proxy); + PhysicsEntityTypeRegistry.registerEventTypes(proxy); + PhysicsEntityTypeRegistry.registerSystemGroups(proxy); + + assertTrue(PhysicsEntityTypes.areEntityStoreTypesRegistered()); + assertTrue(BodyAttachmentComponent.isComponentTypeRegistered()); + assertTrue(GeneratedVisualProxyComponent.isComponentTypeRegistered()); + assertTrue(PhysicsEntityAttachments.isAvailable()); + + PhysicsEntityLifecycle.disable(); + + assertFalse(PhysicsEntityAttachments.isAvailable()); + } finally { + registry.shutdown(); + } + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityProjectionCleanupTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityProjectionCleanupTest.java new file mode 100644 index 00000000..109bf5a2 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/PhysicsEntityProjectionCleanupTest.java @@ -0,0 +1,140 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicsentity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsProjectionIndexResource; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.GeneratedVisualProxyComponent; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Vector3f; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class PhysicsEntityProjectionCleanupTest { + + @AfterEach + void clearTypes() { + PhysicsEntityLifecycle.disable(); + PhysicsEntityTypeRegistry.clearEntityStoreTypes(); + } + + @Test + void cleanAllOwnsAttachmentAndProxyCleanupResult() { + ComponentRegistry registry = new ComponentRegistry<>(); + Store store = store(registry, "physicsentity-clean-all"); + try { + UUID ownedBodyUuid = UUID.randomUUID(); + UUID externalBodyUuid = UUID.randomUUID(); + Ref ownedVisual = addAttachment(store, + ownedBodyUuid, + BodyAttachmentComponent.impulseOwnedVisual(ownedBodyUuid, + new Vector3f(), + new org.joml.Quaternionf(), + Float.NaN)); + Ref external = addAttachment(store, + externalBodyUuid, + BodyAttachmentComponent.externalEntity(externalBodyUuid)); + Ref orphanProxy = addOrphanProxy(store); + + PhysicsEntityProjectionCleanup.Result result = + PhysicsEntityProjectionCleanup.cleanAll(store, List.of()); + + assertFalse(result.skipped()); + assertEquals(1, result.removedAttachmentEntities()); + assertEquals(1, result.detachedExternalAttachments()); + assertEquals(1, result.removedOrphanVisualEntities()); + assertFalse(ownedVisual.isValid()); + assertTrue(external.isValid()); + assertNull(store.getComponent(external, BodyAttachmentComponent.getComponentType())); + assertFalse(orphanProxy.isValid()); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void cleanSelectedFiltersAttachmentsByBody() { + ComponentRegistry registry = new ComponentRegistry<>(); + Store store = store(registry, "physicsentity-clean-selected"); + try { + UUID selectedBodyUuid = UUID.randomUUID(); + UUID ignoredBodyUuid = UUID.randomUUID(); + Ref selected = addAttachment(store, + selectedBodyUuid, + BodyAttachmentComponent.externalEntity(selectedBodyUuid)); + Ref ignored = addAttachment(store, + ignoredBodyUuid, + BodyAttachmentComponent.externalEntity(ignoredBodyUuid)); + PhysicsEntityProjectionCleanup.Result result = + PhysicsEntityProjectionCleanup.cleanSelected(store, + Set.of(selectedBodyUuid), + new org.joml.Vector3d(), + 1.0, + List.of()); + + assertEquals(1, result.detachedExternalAttachments()); + assertEquals(0, result.removedOrphanVisualEntities()); + assertTrue(selected.isValid()); + assertNull(store.getComponent(selected, BodyAttachmentComponent.getComponentType())); + assertNotNull(store.getComponent(ignored, BodyAttachmentComponent.getComponentType())); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Nonnull + private static Store store(@Nonnull ComponentRegistry registry, + @Nonnull String worldName) { + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsEntityTypeRegistry.registerComponentTypes(proxy); + PhysicsEntityTypeRegistry.registerResourceTypes(proxy); + PhysicsEntityTypeRegistry.registerEventTypes(proxy); + PhysicsEntityTypeRegistry.registerSystemGroups(proxy); + PhysicsEntityLifecycle.enable(); + return registry.addStore(new EntityStore(TestInstanceFactory.world(worldName)), + EmptyResourceStorage.get()); + } + + @Nonnull + private static Ref addAttachment(@Nonnull Store store, + @Nonnull UUID bodyUuid, + @Nonnull BodyAttachmentComponent attachment) { + Holder holder = store.getRegistry().newHolder(); + holder.addComponent(BodyAttachmentComponent.getComponentType(), attachment); + Ref ref = store.addEntity(holder, AddReason.SPAWN); + store.getResource(PhysicsProjectionIndexResource.getResourceType()) + .registerAttachment(bodyUuid, ref); + assertNotNull(ref); + return ref; + } + + @Nonnull + private static Ref addOrphanProxy(@Nonnull Store store) { + Holder holder = store.getRegistry().newHolder(); + holder.addComponent(GeneratedVisualProxyComponent.getComponentType(), + new GeneratedVisualProxyComponent()); + Ref ref = store.addEntity(holder, AddReason.SPAWN); + assertNotNull(ref); + return ref; + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsSyncPolicyTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsSyncPolicyTest.java similarity index 81% rename from impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsSyncPolicyTest.java rename to impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsSyncPolicyTest.java index ceeaf340..109cc61c 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsSyncPolicyTest.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsSyncPolicyTest.java @@ -1,10 +1,10 @@ -package dev.hytalemodding.impulse.core.internal.systems.sync; +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.sync; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRuntimeState; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources.PhysicsBodyRuntimeState; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualSyncSettings; import java.util.Arrays; import java.util.List; import org.joml.Quaternionf; @@ -19,12 +19,11 @@ void returnsInitialForUninitializedSyncState() { assertEquals(PhysicsSyncPolicy.SyncDecision.INITIAL, PhysicsSyncPolicy.resolveSyncDecision(syncState, - PhysicsSpaceSettings.defaults(), + new PhysicsVisualSyncSettings(), new Vector3f(), new Quaternionf(), false, false, - false, PhysicsSyncPolicy.SyncRangeTier.NEAR)); } @@ -34,12 +33,11 @@ void returnsTransitionWhenSleepingStateChanges() { assertEquals(PhysicsSyncPolicy.SyncDecision.TRANSITION, PhysicsSyncPolicy.resolveSyncDecision(syncState, - PhysicsSpaceSettings.defaults(), + new PhysicsVisualSyncSettings(), new Vector3f(), new Quaternionf(), true, false, - false, PhysicsSyncPolicy.SyncRangeTier.NEAR)); } @@ -49,43 +47,39 @@ void farRangeFollowersSkipVisualSync() { assertEquals(PhysicsSyncPolicy.SyncDecision.SKIP_VISUAL_RANGE, PhysicsSyncPolicy.resolveSyncDecision(syncState, - PhysicsSpaceSettings.defaults(), + new PhysicsVisualSyncSettings(), new Vector3f(), new Quaternionf(), false, false, - false, PhysicsSyncPolicy.SyncRangeTier.FAR)); } @Test - void lowSpeedNearBodiesUseVisualDeadzoneBeforeKeepalive() { + void lowSpeedNearBodiesUseNormalNearThreshold() { PhysicsBodyRuntimeState.BodySyncState syncState = initializedState(false); syncState.recordSkip(1.0f); - assertEquals(PhysicsSyncPolicy.SyncDecision.SKIP_VISUAL_DEADZONE, + assertEquals(PhysicsSyncPolicy.SyncDecision.THRESHOLD, PhysicsSyncPolicy.resolveSyncDecision(syncState, - PhysicsSpaceSettings.defaults(), + new PhysicsVisualSyncSettings(), new Vector3f(0.05f, 0.0f, 0.0f), new Quaternionf(), false, true, - false, PhysicsSyncPolicy.SyncRangeTier.NEAR)); } @Test - void lowSpeedNearBodiesTriggerKeepaliveAtLongerInterval() { + void activeNearBodiesSyncEveryTickBelowThreshold() { PhysicsBodyRuntimeState.BodySyncState syncState = initializedState(false); - syncState.recordSkip(1.25f); - assertEquals(PhysicsSyncPolicy.SyncDecision.KEEPALIVE, + assertEquals(PhysicsSyncPolicy.SyncDecision.THRESHOLD, PhysicsSyncPolicy.resolveSyncDecision(syncState, - PhysicsSpaceSettings.defaults(), - new Vector3f(0.05f, 0.0f, 0.0f), + new PhysicsVisualSyncSettings(), + new Vector3f(0.001f, 0.0f, 0.0f), new Quaternionf(), false, - true, false, PhysicsSyncPolicy.SyncRangeTier.NEAR)); } @@ -97,30 +91,28 @@ void midRangeFollowersUseCoarseThresholdAndKeepalive() { assertEquals(PhysicsSyncPolicy.SyncDecision.SKIP_VISUAL_RANGE, PhysicsSyncPolicy.resolveSyncDecision(syncState, - PhysicsSpaceSettings.defaults(), + new PhysicsVisualSyncSettings(), new Vector3f(0.2f, 0.0f, 0.0f), new Quaternionf(), false, false, - false, PhysicsSyncPolicy.SyncRangeTier.MID)); syncState.recordSkip(0.1f); assertEquals(PhysicsSyncPolicy.SyncDecision.KEEPALIVE, PhysicsSyncPolicy.resolveSyncDecision(syncState, - PhysicsSpaceSettings.defaults(), + new PhysicsVisualSyncSettings(), new Vector3f(0.2f, 0.0f, 0.0f), new Quaternionf(), false, false, - false, PhysicsSyncPolicy.SyncRangeTier.MID)); } @Test void midRangeFollowersRespectConfiguredMinimumInterval() { - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - settings.getVisualSyncSettings().setVisualMidSyncIntervalTicks(4); + PhysicsVisualSyncSettings settings = new PhysicsVisualSyncSettings(); + settings.setVisualMidSyncIntervalTicks(4); PhysicsBodyRuntimeState.BodySyncState syncState = initializedState(false); syncState.recordSkip(0.15f); @@ -131,7 +123,6 @@ void midRangeFollowersRespectConfiguredMinimumInterval() { new Quaternionf(), false, false, - false, PhysicsSyncPolicy.SyncRangeTier.MID)); syncState.recordSkip(0.05f); @@ -142,15 +133,14 @@ void midRangeFollowersRespectConfiguredMinimumInterval() { new Quaternionf(), false, false, - false, PhysicsSyncPolicy.SyncRangeTier.MID)); } @Test void farRangeLodUsesConfiguredIntervalWhenCutoffIsDisabled() { - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - settings.getVisualSyncSettings().setVisualFarSyncCutoffEnabled(false); - settings.getVisualSyncSettings().setVisualFarSyncIntervalTicks(40); + PhysicsVisualSyncSettings settings = new PhysicsVisualSyncSettings(); + settings.setVisualFarSyncCutoffEnabled(false); + settings.setVisualFarSyncIntervalTicks(40); PhysicsBodyRuntimeState.BodySyncState syncState = initializedState(false); syncState.recordSkip(1.95f); @@ -161,7 +151,6 @@ void farRangeLodUsesConfiguredIntervalWhenCutoffIsDisabled() { new Quaternionf(), false, false, - false, PhysicsSyncPolicy.SyncRangeTier.FAR)); syncState.recordSkip(0.05f); @@ -172,22 +161,20 @@ void farRangeLodUsesConfiguredIntervalWhenCutoffIsDisabled() { new Quaternionf(), false, false, - false, PhysicsSyncPolicy.SyncRangeTier.FAR)); } @Test - void controlledBodiesBypassLowSpeedDeadzoneThresholds() { + void kinematicBodiesBypassLowSpeedDeadzoneThresholds() { PhysicsBodyRuntimeState.BodySyncState syncState = initializedState(false); assertEquals(PhysicsSyncPolicy.SyncDecision.THRESHOLD, PhysicsSyncPolicy.resolveSyncDecision(syncState, - PhysicsSpaceSettings.defaults(), + new PhysicsVisualSyncSettings(), new Vector3f(0.05f, 0.0f, 0.0f), new Quaternionf(), false, true, - true, PhysicsSyncPolicy.SyncRangeTier.NEAR)); } @@ -198,12 +185,11 @@ void activeBodiesTriggerThresholdOnRotationChange() { assertEquals(PhysicsSyncPolicy.SyncDecision.THRESHOLD, PhysicsSyncPolicy.resolveSyncDecision(syncState, - PhysicsSpaceSettings.defaults(), + new PhysicsVisualSyncSettings(), new Vector3f(), rotated, false, false, - false, PhysicsSyncPolicy.SyncRangeTier.NEAR)); } @@ -228,18 +214,17 @@ void sleepingBodiesSkipAfterThresholdAndKeepaliveChecks() { assertEquals(PhysicsSyncPolicy.SyncDecision.SKIP_SLEEPING, PhysicsSyncPolicy.resolveSyncDecision(syncState, - PhysicsSpaceSettings.defaults(), + new PhysicsVisualSyncSettings(), new Vector3f(), new Quaternionf(), true, false, - false, PhysicsSyncPolicy.SyncRangeTier.NEAR)); } @Test - void rangeTierReturnsNearForNonLimitedOrControlledVisuals() { - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); + void rangeTierReturnsNearForNonLimitedOrKinematicVisuals() { + PhysicsVisualSyncSettings settings = new PhysicsVisualSyncSettings(); List players = interests(new Vector3f(100.0f, 0.0f, 0.0f)); @@ -257,12 +242,19 @@ void rangeTierReturnsNearForNonLimitedOrControlledVisuals() { true, players, new Vector3f())); + assertEquals(PhysicsSyncPolicy.SyncRangeTier.NEAR, + PhysicsSyncPolicy.resolveRangeTier(settings, + null, + true, + true, + List.of(), + new Vector3f())); } @Test void rangeTierReturnsFarWhenNoPlayersAreInterested() { assertEquals(PhysicsSyncPolicy.SyncRangeTier.FAR, - PhysicsSyncPolicy.resolveRangeTier(PhysicsSpaceSettings.defaults(), + PhysicsSyncPolicy.resolveRangeTier(new PhysicsVisualSyncSettings(), null, true, false, @@ -283,9 +275,9 @@ void rangeTierFallsBackToNearWhenSettingsAreMissing() { @Test void rangeTierDistinguishesNearMidAndFarBands() { - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - settings.getVisualSyncSettings().setVisualFullSyncRadius(10); - settings.getVisualSyncSettings().setVisualMaxSyncRadius(20); + PhysicsVisualSyncSettings settings = new PhysicsVisualSyncSettings(); + settings.setVisualFullSyncRadius(10); + settings.setVisualMaxSyncRadius(20); List players = interests(new Vector3f(0.0f, 0.0f, 0.0f)); diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsSyncSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsSyncSystemTest.java new file mode 100644 index 00000000..1af11d7e --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/physicsentity/systems/sync/PhysicsSyncSystemTest.java @@ -0,0 +1,351 @@ +package dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.sync; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.core.internal.math.PhysicsVisualPoseMath; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources.PhysicsBodyRuntimeState; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent.AttachmentLifecycle; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent.TransformAuthority; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualSyncSettings; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Quaterniond; +import org.joml.Quaternionf; +import org.joml.Vector3d; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class PhysicsSyncSystemTest { + + @Test + void visualPredictionSecondsClampToConfiguredWindow() { + PhysicsVisualSyncSettings settings = new PhysicsVisualSyncSettings(); + settings.setVisualSnapshotPredictionEnabled(true); + settings.setVisualSnapshotPredictionMaxSeconds(0.05f); + + assertEquals(0.05f, + PhysicsSyncPolicy.visualPredictionSeconds(settings, + 1_100_000_000L, + 1_000_000_000L), + 0.0001f); + } + + @Test + void visualPredictionSecondsStayZeroWhenDisabledOrMissingFrame() { + PhysicsVisualSyncSettings settings = new PhysicsVisualSyncSettings(); + settings.setVisualSnapshotPredictionEnabled(true); + + assertEquals(0.0f, + PhysicsSyncPolicy.visualPredictionSeconds(settings, 1_100_000_000L, 0L), + 0.0001f); + settings.setVisualSnapshotPredictionEnabled(false); + assertEquals(0.0f, + PhysicsSyncPolicy.visualPredictionSeconds(settings, + 1_100_000_000L, + 1_000_000_000L), + 0.0001f); + } + + @Test + void bodyTransformSyncOnlyAppliesToBodyAuthoritativeAttachments() { + UUID bodyUuid = UUID.randomUUID(); + + assertTrue(PhysicsTransformAuthority.shouldApplyBodyTransform(new BodyAttachmentComponent(bodyUuid, + TransformAuthority.BODY, + AttachmentLifecycle.EXTERNAL_ENTITY))); + assertFalse(PhysicsTransformAuthority.shouldApplyBodyTransform(new BodyAttachmentComponent(bodyUuid, + TransformAuthority.CONTROLLER, + AttachmentLifecycle.EXTERNAL_ENTITY))); + assertFalse(PhysicsTransformAuthority.shouldApplyBodyTransform(new BodyAttachmentComponent(bodyUuid, + TransformAuthority.ENTITY_KINEMATIC, + AttachmentLifecycle.EXTERNAL_ENTITY))); + } + + @Test + void bodyTransformSyncSkipsUnchangedHytaleTransformWrites() { + UUID bodyUuid = UUID.randomUUID(); + TransformComponent transform = new TransformComponent(); + BodyAttachmentComponent attachment = new BodyAttachmentComponent(bodyUuid, + TransformAuthority.BODY, + AttachmentLifecycle.EXTERNAL_ENTITY); + PhysicsBodySnapshot snapshot = PhysicsBodySnapshot.of(bodyUuid, + UUID.randomUUID(), + PhysicsBodyType.DYNAMIC, + 1.0f, + 2.0f, + 3.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + false); + PhysicsSyncSystem.Scratch scratch = new PhysicsSyncSystem.Scratch(); + + assertTrue(PhysicsSyncSystem.applyPhysicsStoreSnapshot(transform, + attachment, + snapshot, + scratch)); + assertFalse(PhysicsSyncSystem.applyPhysicsStoreSnapshot(transform, + attachment, + snapshot, + scratch)); + assertEquals(1.0, transform.getPosition().x, 0.0001); + assertEquals(2.0, transform.getPosition().y, 0.0001); + assertEquals(3.0, transform.getPosition().z, 0.0001); + assertEquals(0.0f, transform.getRotation().x(), 0.0001f); + assertEquals(0.0f, transform.getRotation().y(), 0.0001f); + assertEquals(0.0f, transform.getRotation().z(), 0.0001f); + } + + @Test + void policyBackedSyncSkipsGeneratedProxyFarFromPlayers() { + UUID bodyUuid = UUID.randomUUID(); + UUID spaceUuid = UUID.randomUUID(); + TransformComponent transform = new TransformComponent(); + BodyAttachmentComponent attachment = new BodyAttachmentComponent(bodyUuid, + TransformAuthority.BODY, + AttachmentLifecycle.GENERATED_PROXY); + PhysicsSyncSystem.Scratch scratch = new PhysicsSyncSystem.Scratch(); + PhysicsBodyRuntimeState.BodySyncState syncState = + new PhysicsBodyRuntimeState.BodySyncState(); + + PhysicsSyncSystem.SyncResult initial = PhysicsSyncSystem.applyPhysicsStoreSnapshot(transform, + attachment, + snapshot(bodyUuid, spaceUuid, 10.0f, false), + scratch, + syncState, + new PhysicsVisualSyncSettings(), + PhysicsSyncPolicy.SyncRangeTier.NEAR, + 0.05f); + PhysicsSyncSystem.SyncResult skipped = PhysicsSyncSystem.applyPhysicsStoreSnapshot(transform, + attachment, + snapshot(bodyUuid, spaceUuid, 10.5f, false), + scratch, + syncState, + new PhysicsVisualSyncSettings(), + PhysicsSyncPolicy.SyncRangeTier.FAR, + 0.05f); + + assertEquals(PhysicsSyncPolicy.SyncDecision.INITIAL, initial.decision()); + assertTrue(initial.transformChanged()); + assertEquals(PhysicsSyncPolicy.SyncDecision.SKIP_VISUAL_RANGE, skipped.decision()); + assertFalse(skipped.transformChanged()); + assertEquals(10.0, transform.getPosition().x, 0.0001); + assertEquals(0.05f, syncState.getSecondsSinceSync(), 0.0001f); + } + + @Test + void policyBackedSyncTreatsRetargetedBodyAsInitial() { + UUID firstBodyUuid = UUID.randomUUID(); + UUID secondBodyUuid = UUID.randomUUID(); + UUID spaceUuid = UUID.randomUUID(); + TransformComponent transform = new TransformComponent(); + BodyAttachmentComponent attachment = new BodyAttachmentComponent(firstBodyUuid, + TransformAuthority.BODY, + AttachmentLifecycle.EXTERNAL_ENTITY); + PhysicsSyncSystem.Scratch scratch = new PhysicsSyncSystem.Scratch(); + PhysicsBodyRuntimeState.BodySyncState syncState = + new PhysicsBodyRuntimeState.BodySyncState(); + + PhysicsSyncSystem.applyPhysicsStoreSnapshot(transform, + attachment, + snapshot(firstBodyUuid, spaceUuid, 10.0f, false), + scratch, + syncState, + new PhysicsVisualSyncSettings(), + PhysicsSyncPolicy.SyncRangeTier.NEAR, + 0.05f); + PhysicsSyncSystem.SyncResult retargeted = PhysicsSyncSystem.applyPhysicsStoreSnapshot(transform, + attachment, + snapshot(secondBodyUuid, spaceUuid, 10.01f, false), + scratch, + syncState, + new PhysicsVisualSyncSettings(), + PhysicsSyncPolicy.SyncRangeTier.NEAR, + 0.05f); + + assertEquals(PhysicsSyncPolicy.SyncDecision.INITIAL, retargeted.decision()); + assertTrue(retargeted.transformChanged()); + assertEquals(10.01, transform.getPosition().x, 0.0001); + } + + @Test + void rotatingNearDynamicBodiesDoNotUseLowSpeedDeadzone() { + UUID bodyUuid = UUID.randomUUID(); + UUID spaceUuid = UUID.randomUUID(); + TransformComponent transform = new TransformComponent(); + BodyAttachmentComponent attachment = new BodyAttachmentComponent(bodyUuid, + TransformAuthority.BODY, + AttachmentLifecycle.EXTERNAL_ENTITY); + PhysicsSyncSystem.Scratch scratch = new PhysicsSyncSystem.Scratch(); + PhysicsBodyRuntimeState.BodySyncState syncState = + new PhysicsBodyRuntimeState.BodySyncState(); + + PhysicsSyncSystem.applyPhysicsStoreSnapshot(transform, + attachment, + snapshot(bodyUuid, spaceUuid, 10.0f, new Quaternionf(), false), + scratch, + syncState, + new PhysicsVisualSyncSettings(), + PhysicsSyncPolicy.SyncRangeTier.NEAR, + 0.05f); + + PhysicsSyncSystem.SyncResult rotated = PhysicsSyncSystem.applyPhysicsStoreSnapshot(transform, + attachment, + snapshot(bodyUuid, + spaceUuid, + 10.0f, + new Quaternionf().rotateY((float) Math.toRadians(4.0)), + false), + scratch, + syncState, + new PhysicsVisualSyncSettings(), + PhysicsSyncPolicy.SyncRangeTier.NEAR, + 0.05f); + + assertEquals(PhysicsSyncPolicy.SyncDecision.THRESHOLD, rotated.decision()); + assertTrue(rotated.transformChanged()); + } + + @Test + void nonFiniteSnapshotRotationWritesFiniteFallbackRotation() { + UUID bodyUuid = UUID.randomUUID(); + UUID spaceUuid = UUID.randomUUID(); + TransformComponent transform = new TransformComponent(); + transform.getRotation().set(0.1815971f, 1.2601684f, Float.NaN); + BodyAttachmentComponent attachment = new BodyAttachmentComponent(bodyUuid, + TransformAuthority.BODY, + AttachmentLifecycle.EXTERNAL_ENTITY); + PhysicsSyncSystem.Scratch scratch = new PhysicsSyncSystem.Scratch(); + Quaternionf invalidRotation = new Quaternionf(Float.NaN, 0.0f, 0.0f, 1.0f); + + assertTrue(PhysicsSyncSystem.applyPhysicsStoreSnapshot(transform, + attachment, + snapshot(bodyUuid, spaceUuid, 10.0f, invalidRotation, false), + scratch)); + + assertTrue(Float.isFinite(transform.getRotation().x())); + assertTrue(Float.isFinite(transform.getRotation().y())); + assertTrue(Float.isFinite(transform.getRotation().z())); + } + + @Test + void visualPositionKeepsCenterOfMassOffsetWorldUp() { + Vector3f visualPosition = PhysicsVisualPoseMath.visualPositionFromBodyPose(new Vector3f(10.0f, + 20.0f, + 30.0f), + new Quaternionf().rotateZ((float) (Math.PI / 2.0)), + 0.5f, + new Vector3f(), + new Vector3f()); + + assertEquals(10.0f, visualPosition.x, 0.0001f); + assertEquals(19.5f, visualPosition.y, 0.0001f); + assertEquals(30.0f, visualPosition.z, 0.0001f); + } + + @Test + void visualPositionRotatesLocalAttachmentOffset() { + Vector3f visualPosition = PhysicsVisualPoseMath.visualPositionFromBodyPose(new Vector3f(10.0f, + 20.0f, + 30.0f), + new Quaternionf().rotateZ((float) (Math.PI / 2.0)), + 0.5f, + new Vector3f(1.0f, 0.0f, 0.0f), + new Vector3f()); + + assertEquals(10.0f, visualPosition.x, 0.0001f); + assertEquals(20.5f, visualPosition.y, 0.0001f); + assertEquals(30.0f, visualPosition.z, 0.0001f); + } + + @Test + void bodyCenterInvertsWorldUpCenterOfMassOffsetAndRotatedLocalOffset() { + Vector3f bodyCenter = new Vector3f(10.0f, 20.0f, 30.0f); + Quaternionf bodyRotation = new Quaternionf().rotateZ((float) (Math.PI / 2.0)); + float centerOfMassOffsetY = 0.5f; + Vector3f visualPosition = PhysicsVisualPoseMath.visualPositionFromBodyPose(bodyCenter, + bodyRotation, + centerOfMassOffsetY, + new Vector3f(1.0f, 0.0f, 0.0f), + new Vector3f()); + + Vector3d invertedCenter = PhysicsVisualPoseMath.bodyCenterFromVisualPose( + new Vector3d(visualPosition.x, visualPosition.y, visualPosition.z), + new Quaterniond(bodyRotation), + centerOfMassOffsetY, + new Vector3f(1.0f, 0.0f, 0.0f), + new Vector3d()); + + assertEquals(bodyCenter.x, invertedCenter.x, 0.0001f); + assertEquals(bodyCenter.y, invertedCenter.y, 0.0001f); + assertEquals(bodyCenter.z, invertedCenter.z, 0.0001f); + } + + @Test + void attachmentVisualOriginOffsetOverridesBodyShapeOffset() { + BodyAttachmentComponent attachment = BodyAttachmentComponent.externalEntity( + UUID.randomUUID(), + new Vector3f(0.0f, -0.5f, 0.0f), + new Quaternionf(), + 0.5f); + + Vector3f visualPosition = PhysicsVisualPoseMath.visualPositionFromBodyPose(new Vector3f(10.0f, + 20.0f, + 30.0f), + new Quaternionf(), + attachment.resolveVisualOriginOffsetY(1.0f), + attachment.getLocalPositionOffset(), + new Vector3f()); + + assertEquals(10.0f, visualPosition.x, 0.0001f); + assertEquals(19.0f, visualPosition.y, 0.0001f); + assertEquals(30.0f, visualPosition.z, 0.0001f); + assertEquals(0.5f, attachment.clone().getVisualOriginOffsetY(), 0.0001f); + } + + private static PhysicsBodySnapshot snapshot(@Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid, + float positionX, + boolean sleeping) { + return snapshot(bodyUuid, spaceUuid, positionX, new Quaternionf(), sleeping); + } + + private static PhysicsBodySnapshot snapshot(@Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid, + float positionX, + @Nonnull Quaternionf rotation, + boolean sleeping) { + return PhysicsBodySnapshot.of(bodyUuid, + spaceUuid, + PhysicsBodyType.DYNAMIC, + positionX, + 2.0f, + 3.0f, + rotation.x, + rotation.y, + rotation.z, + rotation.w, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + sleeping); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldCollisionLifecycleTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldCollisionLifecycleTest.java deleted file mode 100644 index 903398b6..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldCollisionLifecycleTest.java +++ /dev/null @@ -1,308 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.FakePhysicsBackendRuntime; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistration; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import it.unimi.dsi.fastutil.ints.Int2ObjectMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; -import javax.annotation.Nonnull; -import org.joml.Vector3d; -import org.joml.Vector3f; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -@SuppressWarnings("DataFlowIssue") -class WorldCollisionLifecycleTest { - - private static final AtomicInteger BACKEND_COUNTER = new AtomicInteger(); - - @BeforeEach - @AfterEach - void disableLifecycle() { - WorldCollisionLifecycle.disable(); - } - - @Test - void lifecycleStartsDisabledAndRejectsManualBuilds() { - PhysicsWorldRuntimeResource resource = new PhysicsWorldRuntimeResource(); - SpaceId spaceId = createSpace(resource, PhysicsSpaceSettings.streamingWorldCollision()); - - assertFalse(WorldCollisionLifecycle.isEnabled()); - assertThrows(IllegalStateException.class, - () -> resource.rebuildWorldCollisionAround(null, spaceId, new Vector3d(), 1)); - assertThrows(IllegalStateException.class, - () -> resource.ensureWorldCollisionAround(null, spaceId, List.of(new Vector3d()), 1, 0L)); - assertDoesNotThrow(() -> resource.clearWorldCollision(spaceId)); - assertDoesNotThrow(resource::getWorldCollisionStats); - } - - @Test - void enabledLifecycleStillRequiresSpaceOptIn() { - WorldCollisionLifecycle.enable(); - PhysicsWorldRuntimeResource resource = new PhysicsWorldRuntimeResource(); - SpaceId spaceId = createSpace(resource, PhysicsSpaceSettings.defaults()); - - assertThrows(IllegalStateException.class, - () -> resource.rebuildWorldCollisionAround(null, spaceId, new Vector3d(), 1)); - assertThrows(IllegalStateException.class, - () -> resource.ensureWorldCollisionAround(null, spaceId, List.of(new Vector3d()), 1, 0L)); - assertDoesNotThrow(() -> resource.clearWorldCollision(spaceId)); - } - - @Test - void lifecycleGenerationChangesWhenLifecycleIsDisabled() { - WorldCollisionLifecycle.enable(); - long enabledGeneration = WorldCollisionLifecycle.generation(); - - WorldCollisionLifecycle.disable(); - - assertTrue(WorldCollisionLifecycle.generation() > enabledGeneration); - } - - @Test - void disablingLifecycleRestoresChunkBoundaryPausedBodies() { - WorldCollisionLifecycle.enable(); - RuntimeFixture fixture = createRuntimeFixture(PhysicsSpaceSettings.streamingWorldCollision()); - RigidBodyKey bodyKey = spawnDynamicBody(fixture.resource(), fixture.spaceId()); - BodyHandle body = bodyHandle(fixture.resource(), bodyKey); - fixture.runtime().setBodyType(body.backendSpaceId(), - body.backendBodyId(), - BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.KINEMATIC)); - fixture.resource().pauseChunkBoundaryBody(bodyKey, - 42L, - PhysicsBodyType.DYNAMIC, - new Vector3f(1.0f, 2.0f, 3.0f), - new Vector3f(4.0f, 5.0f, 6.0f)); - - WorldCollisionLifecycle.disable(); - - assertEquals(BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC), - fixture.runtime().bodyTypeCode(body.backendSpaceId(), body.backendBodyId())); - assertNull(fixture.resource().getChunkBoundaryPauseState(bodyKey)); - } - - @Test - void disablingLifecycleRestoresCollisionLodFilters() { - WorldCollisionLifecycle.enable(); - RuntimeFixture fixture = createRuntimeFixture(PhysicsSpaceSettings.streamingWorldCollision()); - RigidBodyKey bodyKey = spawnDynamicBody(fixture.resource(), fixture.spaceId()); - BodyHandle body = bodyHandle(fixture.resource(), bodyKey); - fixture.runtime().setBodyCollisionFilter(body.backendSpaceId(), - body.backendBodyId(), - PhysicsCollisionFilters.DYNAMIC_BODY, - PhysicsCollisionFilters.TERRAIN); - - WorldCollisionLifecycle.disable(); - - assertEquals(PhysicsCollisionFilters.DYNAMIC_BODY, - fixture.runtime().bodyCollisionGroup(body.backendSpaceId(), body.backendBodyId())); - assertEquals(PhysicsCollisionFilters.TERRAIN | PhysicsCollisionFilters.DYNAMIC_BODY, - fixture.runtime().bodyCollisionMask(body.backendSpaceId(), body.backendBodyId())); - } - - @Test - void disablingLifecycleDestroysRetainedTerrainBodiesBeforeClearingCache() throws Exception { - WorldCollisionLifecycle.enable(); - RuntimeFixture fixture = createRuntimeFixture(PhysicsSpaceSettings.streamingWorldCollision()); - PhysicsSpaceBinding space = requireSpaceBinding(fixture); - long backendBodyId = cacheTerrainBody(fixture.resource(), space); - - assertTrue(fixture.runtime().containsBody(space.backendSpaceHandle().value(), backendBodyId)); - assertEquals(1, fixture.resource().getWorldCollisionStats().bodies()); - - WorldCollisionLifecycle.disable(); - - assertFalse(fixture.runtime().containsBody(space.backendSpaceHandle().value(), backendBodyId)); - assertEquals(0, fixture.runtime().bodyCount(space.backendSpaceHandle().value())); - assertEquals(0, fixture.resource().getWorldCollisionStats().bodies()); - assertFalse(fixture.resource().worldCollisionCache().isStreamingApplyPending()); - assertTrue(fixture.resource().worldCollisionCache().tryBeginStreamingApply()); - - WorldCollisionLifecycle.enable(); - assertTrue(WorldCollisionLifecycle.isEnabled()); - } - - private static SpaceId createSpace(PhysicsWorldRuntimeResource resource, - PhysicsSpaceSettings settings) { - BackendId backendId = new BackendId("test:world-collision-lifecycle-" - + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerRuntimeProvider(new FakePhysicsBackendRuntimeProvider(backendId, - false, - false)); - return resource.createSpace(backendId, "test-world", settings); - } - - private static RuntimeFixture createRuntimeFixture(PhysicsSpaceSettings settings) { - BackendId backendId = new BackendId("test:world-collision-cleanup-" - + BACKEND_COUNTER.incrementAndGet()); - FakePhysicsBackendRuntimeProvider provider = - new FakePhysicsBackendRuntimeProvider(backendId, false, false); - Impulse.registerRuntimeProvider(provider); - PhysicsWorldRuntimeResource resource = new PhysicsWorldRuntimeResource(); - SpaceId spaceId = resource.createSpace(backendId, "test-world", settings); - return new RuntimeFixture(resource, spaceId, provider.createdRuntimes().getFirst()); - } - - private static long cacheTerrainBody(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull PhysicsSpaceBinding space) throws Exception { - long backendBodyId = space.runtime().createBody(space.backendSpaceHandle().value(), - BackendRuntimeCodes.SHAPE_BOX, - 0.5f, - 0.5f, - 0.5f, - 0.0f, - 0.0f, - BackendRuntimeCodes.AXIS_Y, - 0.0f, - 0.0f, - BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.STATIC), - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 1.0f); - Object spaceCache = newSpaceCollisionCache(); - Object section = newCachedSection(0, 0, 0); - markBackendBody(section, backendBodyId); - putCachedSection(spaceCache, section); - putSpaceCache(resource.worldCollisionCache(), space.spaceId(), spaceCache); - return backendBodyId; - } - - private static Object newSpaceCollisionCache() throws Exception { - Class cacheType = nestedCollisionCacheClass("SpaceCollisionCache"); - Constructor constructor = cacheType.getDeclaredConstructor(); - constructor.setAccessible(true); - return constructor.newInstance(); - } - - private static Object newCachedSection(int chunkX, int sectionY, int chunkZ) throws Exception { - Class sectionType = nestedCollisionCacheClass("CachedSection"); - Constructor constructor = sectionType.getDeclaredConstructor(int.class, - int.class, - int.class, - long.class, - long.class); - constructor.setAccessible(true); - return constructor.newInstance(chunkX, sectionY, chunkZ, 0L, 1L); - } - - @SuppressWarnings("unchecked") - private static void markBackendBody(@Nonnull Object section, long backendBodyId) - throws Exception { - Field backendBodyIds = - section.getClass().getDeclaredField("backendBodyIds"); - backendBodyIds.setAccessible(true); - ((List) backendBodyIds.get(section)).add(backendBodyId); - } - - @SuppressWarnings("unchecked") - private static void putCachedSection(@Nonnull Object cache, @Nonnull Object section) - throws Exception { - Method keyMethod = WorldVoxelCollisionCache.class.getDeclaredMethod("packSectionKey", - int.class, - int.class, - int.class); - keyMethod.setAccessible(true); - long key = (long) keyMethod.invoke(null, - intField(section, "chunkX"), - intField(section, "sectionY"), - intField(section, "chunkZ")); - Field sectionsField = cache.getClass().getDeclaredField("sections"); - sectionsField.setAccessible(true); - ((Long2ObjectMap) sectionsField.get(cache)).put(key, section); - } - - @SuppressWarnings("unchecked") - private static void putSpaceCache(@Nonnull WorldVoxelCollisionCache cache, - @Nonnull SpaceId spaceId, - @Nonnull Object spaceCache) throws Exception { - Field spaces = WorldVoxelCollisionCache.class.getDeclaredField("spaces"); - spaces.setAccessible(true); - ((Int2ObjectMap) spaces.get(cache)).put(spaceId.value(), spaceCache); - } - - private static int intField(@Nonnull Object target, @Nonnull String name) throws Exception { - Field field = target.getClass().getDeclaredField(name); - field.setAccessible(true); - return field.getInt(target); - } - - @Nonnull - private static Class nestedCollisionCacheClass(@Nonnull String simpleName) { - return Arrays.stream(WorldVoxelCollisionCache.class.getDeclaredClasses()) - .filter(candidate -> candidate.getSimpleName().equals(simpleName)) - .findFirst() - .orElseThrow(); - } - - private static RigidBodyKey spawnDynamicBody(PhysicsWorldRuntimeResource resource, - SpaceId spaceId) { - RigidBodyKey bodyKey = RigidBodyKey.random(); - assertTrue(resource.submitCommands(1L, commands -> commands - .spawnBody(bodyKey, spawn -> spawn - .space(spaceId) - .box(0.5f, 0.5f, 0.5f) - .mass(1.0f) - .dynamic() - .kind(PhysicsBodyKind.BODY) - .runtimeOnly())) - .allApplied() - .toCompletableFuture() - .join()); - return bodyKey; - } - - @Nonnull - private static PhysicsSpaceBinding requireSpaceBinding(@Nonnull RuntimeFixture fixture) { - PhysicsSpaceBinding space = fixture.resource().getSpaceBinding(fixture.spaceId()); - assertNotNull(space); - return space; - } - - private static BodyHandle bodyHandle(PhysicsWorldRuntimeResource resource, - RigidBodyKey bodyKey) { - PhysicsBodyRegistration registration = resource.getRegistration(bodyKey); - assertNotNull(registration); - PhysicsSpaceBinding space = resource.getSpaceBinding(registration.spaceId()); - assertNotNull(space); - return new BodyHandle(space.backendSpaceHandle().value(), - registration.backendBodyHandle().value()); - } - - private record RuntimeFixture(PhysicsWorldRuntimeResource resource, - SpaceId spaceId, - FakePhysicsBackendRuntime runtime) { - } - - private record BodyHandle(int backendSpaceId, long backendBodyId) { - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldVoxelCollisionCacheTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldVoxelCollisionCacheTest.java deleted file mode 100644 index 81ca106b..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/WorldVoxelCollisionCacheTest.java +++ /dev/null @@ -1,707 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import com.hypixel.hytale.math.util.ChunkUtil; -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.CombineCall; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.FakePhysicsBackendRuntime; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.VoxelTerrainCall; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.SectionCollisionGeometry; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldCollisionBuildOptions; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldCollisionStreamingBounds; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldVoxelCollisionCache; -import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.SectionCollisionGeometry.BoxCollider; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodySnapshots; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldCollisionSettings; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import javax.annotation.Nonnull; -import org.joml.Vector3d; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class WorldVoxelCollisionCacheTest { - - @Test - void streamingApplyGateAllowsOnlyOnePendingMutation() { - WorldVoxelCollisionCache cache = new WorldVoxelCollisionCache(); - - assertFalse(cache.isStreamingApplyPending()); - assertTrue(cache.tryBeginStreamingApply()); - assertTrue(cache.isStreamingApplyPending()); - assertFalse(cache.tryBeginStreamingApply()); - - cache.finishStreamingApply(); - - assertFalse(cache.isStreamingApplyPending()); - assertTrue(cache.tryBeginStreamingApply()); - } - - @Test - void copyFromDoesNotInheritPendingStreamingApply() { - WorldVoxelCollisionCache source = new WorldVoxelCollisionCache(); - WorldVoxelCollisionCache target = new WorldVoxelCollisionCache(); - - assertTrue(source.tryBeginStreamingApply()); - - target.copyFrom(source); - - assertFalse(target.isStreamingApplyPending()); - } - - @Test - void copyFromDeepCopiesCachedSectionBodyIds() throws Exception { - RuntimeFixture fixture = runtimeFixture("test:copy-section-isolation", true); - WorldVoxelCollisionCache source = new WorldVoxelCollisionCache(); - WorldVoxelCollisionCache target = new WorldVoxelCollisionCache(); - Object spaceCache = newSpaceCollisionCache(); - Object sourceSection = newCachedSection(1, 2, 3); - long copiedBodyId = createVoxelTerrain(fixture); - markVoxelTerrain(sourceSection, copiedBodyId); - putCachedSection(spaceCache, sourceSection); - putSpaceCache(source, fixture.binding().spaceId(), spaceCache); - - target.copyFrom(source); - long sourceOnlyBodyId = createVoxelTerrain(fixture); - addBackendBodyId(sourceSection, sourceOnlyBodyId); - - assertEquals(2, source.bodyCount(fixture.binding().spaceId())); - assertEquals(1, target.bodyCount(fixture.binding().spaceId())); - assertTrue(target.containsBody(fixture.binding().spaceId(), copiedBodyId)); - assertFalse(target.containsBody(fixture.binding().spaceId(), sourceOnlyBodyId)); - } - - @Test - void bodyTargetCacheRefreshesActiveBodiesEveryFourTicks() { - WorldVoxelCollisionCache cache = new WorldVoxelCollisionCache(); - WorldCollisionProfilingResource.Snapshot snapshot = - new WorldCollisionProfilingResource.Snapshot(); - SpaceId spaceId = new SpaceId(1001); - RigidBodyKey bodyId = bodyId(1); - WorldCollisionStreamingBounds bounds = boundsAt(10.0f, 65.0f, 10.0f); - - assertTrue(cache.shouldRefreshBodyTarget(spaceId, bodyId, bounds, false, 1L, 100, snapshot) - .refresh()); - cache.recordBodyTargetRefresh(spaceId, bodyId, bounds, false, 1L); - assertFalse(cache.shouldRefreshBodyTarget(spaceId, bodyId, bounds, false, 2L, 100, snapshot) - .refresh()); - assertFalse(cache.shouldRefreshBodyTarget(spaceId, bodyId, bounds, false, 3L, 100, snapshot) - .refresh()); - assertFalse(cache.shouldRefreshBodyTarget(spaceId, bodyId, bounds, false, 4L, 100, snapshot) - .refresh()); - assertTrue(cache.shouldRefreshBodyTarget(spaceId, bodyId, bounds, false, 5L, 100, snapshot) - .refresh()); - cache.recordBodyTargetRefresh(spaceId, bodyId, bounds, false, 5L); - - assertEquals(1, snapshot.getBodyTargetFirstSeen()); - assertEquals(4, snapshot.getBodyTargetCacheHits()); - assertEquals(3, snapshot.getBodyTargetActiveStableSkips()); - assertEquals(1, snapshot.getBodyTargetActiveRefreshes()); - } - - @Test - void bodyTargetCacheRefreshesSleepingBodiesOnTtlBoundedInterval() { - WorldVoxelCollisionCache cache = new WorldVoxelCollisionCache(); - WorldCollisionProfilingResource.Snapshot snapshot = - new WorldCollisionProfilingResource.Snapshot(); - SpaceId spaceId = new SpaceId(1002); - RigidBodyKey bodyId = bodyId(2); - WorldCollisionStreamingBounds bounds = boundsAt(10.0f, 65.0f, 10.0f); - - assertTrue(cache.shouldRefreshBodyTarget(spaceId, bodyId, bounds, true, 1L, 100, snapshot) - .refresh()); - cache.recordBodyTargetRefresh(spaceId, bodyId, bounds, true, 1L); - assertFalse(cache.shouldRefreshBodyTarget(spaceId, bodyId, bounds, true, 2L, 100, snapshot) - .refresh()); - assertTrue(cache.shouldRefreshBodyTarget(spaceId, bodyId, bounds, true, 21L, 100, snapshot) - .refresh()); - cache.recordBodyTargetRefresh(spaceId, bodyId, bounds, true, 21L); - - assertEquals(1, snapshot.getBodyTargetFirstSeen()); - assertEquals(2, snapshot.getBodyTargetCacheHits()); - assertEquals(1, snapshot.getBodyTargetSleepingStableSkips()); - assertEquals(1, snapshot.getBodyTargetSleepingRefreshes()); - } - - @Test - void bodyTargetCacheRefreshesImmediatelyWhenBoundsChange() { - WorldVoxelCollisionCache cache = new WorldVoxelCollisionCache(); - WorldCollisionProfilingResource.Snapshot snapshot = - new WorldCollisionProfilingResource.Snapshot(); - SpaceId spaceId = new SpaceId(1003); - RigidBodyKey bodyId = bodyId(3); - - assertTrue(cache.shouldRefreshBodyTarget(spaceId, - bodyId, - boundsAt(10.0f, 65.0f, 10.0f), - false, - 1L, - 100, - snapshot).refresh()); - cache.recordBodyTargetRefresh(spaceId, - bodyId, - boundsAt(10.0f, 65.0f, 10.0f), - false, - 1L); - assertTrue(cache.shouldRefreshBodyTarget(spaceId, - bodyId, - boundsAt(40.0f, 65.0f, 10.0f), - false, - 2L, - 100, - snapshot).refresh()); - cache.recordBodyTargetRefresh(spaceId, - bodyId, - boundsAt(40.0f, 65.0f, 10.0f), - false, - 2L); - - assertEquals(1, snapshot.getBodyTargetFirstSeen()); - assertEquals(1, snapshot.getBodyTargetCacheHits()); - assertEquals(1, snapshot.getBodyTargetBoundsChanged()); - } - - @Test - void bodyTargetRefreshIsNotConsumedUntilTerrainApplyRecordsIt() { - WorldVoxelCollisionCache cache = new WorldVoxelCollisionCache(); - SpaceId spaceId = new SpaceId(1005); - RigidBodyKey bodyId = bodyId(5); - WorldCollisionStreamingBounds bounds = boundsAt(10.0f, 65.0f, 10.0f); - - assertTrue(cache.shouldRefreshBodyTarget(spaceId, bodyId, bounds, false, 1L, 100, null) - .refresh()); - assertTrue(cache.shouldRefreshBodyTarget(spaceId, bodyId, bounds, false, 2L, 100, null) - .refresh()); - - cache.recordBodyTargetRefresh(spaceId, bodyId, bounds, false, 2L); - - assertFalse(cache.shouldRefreshBodyTarget(spaceId, bodyId, bounds, false, 3L, 100, null) - .refresh()); - } - - @Test - void bodyTargetCachePrunesBodiesThatDisappearPastDoubleTtl() { - WorldVoxelCollisionCache cache = new WorldVoxelCollisionCache(); - WorldCollisionProfilingResource.Snapshot snapshot = - new WorldCollisionProfilingResource.Snapshot(); - SpaceId spaceId = new SpaceId(1004); - RigidBodyKey bodyId = bodyId(4); - WorldCollisionStreamingBounds bounds = boundsAt(10.0f, 65.0f, 10.0f); - - assertTrue(cache.shouldRefreshBodyTarget(spaceId, bodyId, bounds, false, 1L, 100, snapshot) - .refresh()); - cache.recordBodyTargetRefresh(spaceId, bodyId, bounds, false, 1L); - assertEquals(1, cache.pruneBodyStreamingTargets(spaceId, 202L, 100, snapshot)); - assertTrue(cache.shouldRefreshBodyTarget(spaceId, bodyId, bounds, false, 203L, 100, snapshot) - .refresh()); - cache.recordBodyTargetRefresh(spaceId, bodyId, bounds, false, 203L); - - assertEquals(2, snapshot.getBodyTargetFirstSeen()); - assertEquals(1, snapshot.getBodyTargetsPruned()); - } - - @Test - void absentVoxelCapabilityFallsBackToMergedFullCubeBoxes() throws Exception { - RuntimeFixture fixture = runtimeFixture("test:voxel-runtime-fallback", false); - SectionCollisionGeometry geometry = new SectionCollisionGeometry(new int[] {0, 0, 0, 1, 0, 0}, - List.of(new BoxCollider(1.0, 0.5, 0.5, 1.0, 0.5, 0.5)), - List.of(new BoxCollider(4.25, 2.25, 4.25, 0.25, 0.25, 0.25)), - 4096, - 2, - 0, - 1); - Object cachedSection = newCachedSection(); - - assertDoesNotThrow(() -> addGeometryBodies(fixture.binding(), - cachedSection, - geometry, - 0, - 0, - 0)); - - WorldVoxelCollisionCache.DebugSection debugSection = debugSection(cachedSection); - assertEquals(2, fixture.runtime().bodyCount(fixture.backendSpaceId())); - assertEquals(0, fixture.runtime().voxelTerrainCalls(fixture.backendSpaceId()).size()); - assertFalse(debugSection.voxelTerrain()); - assertEquals(1, debugSection.fullCubeBoxes().size()); - assertEquals(1, debugSection.detailBoxes().size()); - } - - @Test - void supportedRuntimeCreatesVoxelTerrainAndKeepsDebugBoxes() throws Throwable { - RuntimeFixture fixture = runtimeFixture("test:voxel-runtime-native", true); - SectionCollisionGeometry geometry = new SectionCollisionGeometry(new int[] {0, 0, 0, 1, 0, 0}, - List.of(new BoxCollider(40.0, 48.5, 64.0, 1.0, 0.5, 0.5)), - List.of(new BoxCollider(36.25, 50.25, 68.25, 0.25, 0.25, 0.25)), - 4096, - 2, - 0, - 1); - Object cachedSection = newCachedSection(2, 3, 4); - - addGeometryBodies(fixture.binding(), cachedSection, geometry, 2, 3, 4); - - List calls = fixture.runtime().voxelTerrainCalls(fixture.backendSpaceId()); - WorldVoxelCollisionCache.DebugSection debugSection = debugSection(cachedSection); - assertEquals(1, calls.size()); - assertArrayEquals(new int[] {0, 0, 0, 1, 0, 0}, calls.getFirst().voxelCoordinates()); - assertEquals((float) (2 << ChunkUtil.BITS), calls.getFirst().positionX()); - assertEquals((float) (3 << ChunkUtil.BITS), calls.getFirst().positionY()); - assertEquals((float) (4 << ChunkUtil.BITS), calls.getFirst().positionZ()); - assertEquals(0.75f, calls.getFirst().friction()); - assertEquals(0.0f, calls.getFirst().restitution()); - assertEquals(PhysicsCollisionFilters.TERRAIN, calls.getFirst().collisionGroup()); - assertEquals(PhysicsCollisionFilters.ALL, calls.getFirst().collisionMask()); - assertEquals(2, fixture.runtime().bodyCount(fixture.backendSpaceId())); - assertTrue(debugSection.voxelTerrain()); - assertEquals(1, debugSection.fullCubeBoxes().size()); - assertEquals(1, debugSection.detailBoxes().size()); - } - - @Test - void terrainMaterialSettingsApplyToNativeVoxelAndFallbackBoxes() throws Throwable { - PhysicsWorldCollisionSettings nativeSettings = new PhysicsWorldCollisionSettings(); - nativeSettings.setNativeVoxelTerrainEnabled(true); - nativeSettings.setTerrainMaterial(0.9f, 0.25f); - RuntimeFixture nativeFixture = runtimeFixture("test:voxel-runtime-custom-material", true); - SectionCollisionGeometry nativeGeometry = new SectionCollisionGeometry(new int[] {0, 0, 0}, - List.of(new BoxCollider(1.0, 0.5, 0.5, 1.0, 0.5, 0.5)), - List.of(), - 4096, - 1, - 0, - 0); - Object nativeSection = newCachedSection(); - - addGeometryBodies(nativeFixture.binding(), - nativeSection, - nativeGeometry, - 0, - 0, - 0, - WorldCollisionBuildOptions.fromSettings(nativeSettings)); - - List calls = - nativeFixture.runtime().voxelTerrainCalls(nativeFixture.backendSpaceId()); - assertEquals(1, calls.size()); - assertEquals(0.9f, calls.getFirst().friction(), 0.0001f); - assertEquals(0.25f, calls.getFirst().restitution(), 0.0001f); - - PhysicsWorldCollisionSettings fallbackSettings = new PhysicsWorldCollisionSettings(); - fallbackSettings.setTerrainMaterial(0.8f, 0.1f); - RuntimeFixture fallbackFixture = runtimeFixture("test:box-runtime-custom-material", false); - Object fallbackSection = newCachedSection(); - - addGeometryBodies(fallbackFixture.binding(), - fallbackSection, - nativeGeometry, - 0, - 0, - 0, - WorldCollisionBuildOptions.fromSettings(fallbackSettings)); - - long fallbackBodyId = firstBackendBodyId(fallbackSection); - var snapshot = PhysicsBodySnapshots.read(fallbackFixture.binding(), fallbackBodyId); - assertEquals(0.8f, snapshot.friction(), 0.0001f); - assertEquals(0.1f, snapshot.restitution(), 0.0001f); - } - - @Test - void disabledNativeVoxelTerrainFallsBackToMergedFullCubeBoxes() throws Throwable { - RuntimeFixture fixture = runtimeFixture("test:voxel-runtime-disabled", true); - SectionCollisionGeometry geometry = new SectionCollisionGeometry(new int[] {0, 0, 0, 1, 0, 0}, - List.of(new BoxCollider(1.0, 0.5, 0.5, 1.0, 0.5, 0.5)), - List.of(), - 4096, - 2, - 0, - 0); - Object cachedSection = newCachedSection(); - - addGeometryBodies(fixture.binding(), cachedSection, geometry, 0, 0, 0, false); - - WorldVoxelCollisionCache.DebugSection debugSection = debugSection(cachedSection); - assertEquals(1, fixture.runtime().bodyCount(fixture.backendSpaceId())); - assertEquals(0, fixture.runtime().voxelTerrainCalls(fixture.backendSpaceId()).size()); - assertFalse(debugSection.voxelTerrain()); - assertEquals(1, debugSection.fullCubeBoxes().size()); - } - - @Test - void boxFallbackConfigurationFailureRemovesCreatedBackendBody() throws Throwable { - RuntimeFixture fixture = runtimeFixture("test:box-runtime-cleanup", false); - fixture.provider().failNextBodyFriction(new IllegalStateException("forced friction failure")); - SectionCollisionGeometry geometry = new SectionCollisionGeometry(new int[] {0, 0, 0}, - List.of(new BoxCollider(1.0, 0.5, 0.5, 1.0, 0.5, 0.5)), - List.of(), - 4096, - 1, - 0, - 0); - Object cachedSection = newCachedSection(); - - IllegalStateException failure = assertThrows(IllegalStateException.class, - () -> addGeometryBodies(fixture.binding(), cachedSection, geometry, 0, 0, 0)); - removeBuiltSectionAfterFailure(fixture.binding(), cachedSection, failure); - - assertEquals(0, fixture.runtime().bodyCount(fixture.backendSpaceId())); - assertEquals(0, debugSection(cachedSection).bodyCount()); - } - - @Test - void stitchesVoxelTerrainToSixAdjacentSections() throws Throwable { - RuntimeFixture fixture = runtimeFixture("test:voxel-runtime-stitch", true); - Object cache = newSpaceCollisionCache(); - Object center = newCachedSection(10, 5, 20); - long centerBody = createVoxelTerrain(fixture); - markVoxelTerrain(center, centerBody); - putCachedSection(cache, center); - - Object west = cachedVoxelNeighbor(fixture, cache, 9, 5, 20); - Object east = cachedVoxelNeighbor(fixture, cache, 11, 5, 20); - Object down = cachedVoxelNeighbor(fixture, cache, 10, 4, 20); - Object up = cachedVoxelNeighbor(fixture, cache, 10, 6, 20); - Object north = cachedVoxelNeighbor(fixture, cache, 10, 5, 19); - Object south = cachedVoxelNeighbor(fixture, cache, 10, 5, 21); - - stitchAdjacentVoxelTerrains(fixture.binding(), cache, center); - - assertEquals(List.of(new CombineCall(centerBody, voxelTerrainBodyId(west), -16, 0, 0), - new CombineCall(centerBody, voxelTerrainBodyId(east), 16, 0, 0), - new CombineCall(centerBody, voxelTerrainBodyId(down), 0, -16, 0), - new CombineCall(centerBody, voxelTerrainBodyId(up), 0, 16, 0), - new CombineCall(centerBody, voxelTerrainBodyId(north), 0, 0, -16), - new CombineCall(centerBody, voxelTerrainBodyId(south), 0, 0, 16)), - fixture.runtime().combineCalls(fixture.backendSpaceId())); - } - - @Test - void clearSectionsAroundKeepsDistantCachedTerrain() throws Exception { - RuntimeFixture fixture = runtimeFixture("test:section-radius-clear", true); - WorldVoxelCollisionCache worldCache = new WorldVoxelCollisionCache(); - Object spaceCache = newSpaceCollisionCache(); - Object near = newCachedSection(0, 2, 0); - long nearBody = createVoxelTerrain(fixture); - markVoxelTerrain(near, nearBody); - putCachedSection(spaceCache, near); - Object far = newCachedSection(8, 2, 8); - long farBody = createVoxelTerrain(fixture); - markVoxelTerrain(far, farBody); - putCachedSection(spaceCache, far); - putSpaceCache(worldCache, fixture.binding().spaceId(), spaceCache); - - int removed = worldCache.clearSectionsAround(fixture.binding().spaceId(), - fixture.binding(), - new Vector3d(8.0, 65.0, 8.0), - 8); - - assertEquals(1, removed); - assertEquals(1, worldCache.sectionCount()); - assertFalse(worldCache.containsBody(fixture.binding().spaceId(), nearBody)); - assertTrue(worldCache.containsBody(fixture.binding().spaceId(), farBody)); - assertEquals(1, fixture.runtime().bodyCount(fixture.backendSpaceId())); - } - - private static RigidBodyKey bodyId(long leastSignificantBits) { - return RigidBodyKey.of(new UUID(0L, leastSignificantBits)); - } - - private static WorldCollisionStreamingBounds boundsAt(float x, float y, float z) { - return WorldCollisionStreamingBounds.from(new Vector3f(x, y, z), 4); - } - - private static Object newCachedSection() throws Exception { - return newCachedSection(0, 0, 0); - } - - private static Object newCachedSection(int chunkX, int sectionY, int chunkZ) throws Exception { - Class sectionType = nestedClass("CachedSection"); - Constructor constructor = sectionType.getDeclaredConstructor(int.class, - int.class, - int.class, - long.class, - long.class); - constructor.setAccessible(true); - return constructor.newInstance(chunkX, sectionY, chunkZ, 0L, 1L); - } - - private static void addGeometryBodies(@Nonnull PhysicsSpaceBinding space, - @Nonnull Object target, - @Nonnull SectionCollisionGeometry geometry, - int chunkX, - int sectionY, - int chunkZ) throws Throwable { - addGeometryBodies(space, target, geometry, chunkX, sectionY, chunkZ, true); - } - - private static void addGeometryBodies(@Nonnull PhysicsSpaceBinding space, - @Nonnull Object target, - @Nonnull SectionCollisionGeometry geometry, - int chunkX, - int sectionY, - int chunkZ, - boolean nativeVoxelTerrainEnabled) throws Throwable { - addGeometryBodies(space, - target, - geometry, - chunkX, - sectionY, - chunkZ, - WorldCollisionBuildOptions.fromNativeVoxelTerrainEnabled(nativeVoxelTerrainEnabled)); - } - - private static void addGeometryBodies(@Nonnull PhysicsSpaceBinding space, - @Nonnull Object target, - @Nonnull SectionCollisionGeometry geometry, - int chunkX, - int sectionY, - int chunkZ, - @Nonnull WorldCollisionBuildOptions buildOptions) throws Throwable { - Method method = Arrays.stream(WorldVoxelCollisionCache.class.getDeclaredMethods()) - .filter(candidate -> candidate.getName().equals("addGeometryBodies")) - .findFirst() - .orElseThrow(); - method.setAccessible(true); - try { - method.invoke(null, addGeometryBodiesArguments(method, - space, - target, - geometry, - chunkX, - sectionY, - chunkZ, - buildOptions)); - } catch (InvocationTargetException exception) { - throw exception.getCause(); - } - } - - @Nonnull - private static Object[] addGeometryBodiesArguments(@Nonnull Method method, - @Nonnull PhysicsSpaceBinding space, - @Nonnull Object target, - @Nonnull SectionCollisionGeometry geometry, - int chunkX, - int sectionY, - int chunkZ, - @Nonnull WorldCollisionBuildOptions buildOptions) { - Object[] arguments = new Object[method.getParameterCount()]; - int integerIndex = 0; - for (int index = 0; index < arguments.length; index++) { - Class type = method.getParameterTypes()[index]; - if (type == PhysicsSpaceBinding.class) { - arguments[index] = space; - } else if (type == SectionCollisionGeometry.class) { - arguments[index] = geometry; - } else if (type.getSimpleName().equals("CachedSection")) { - arguments[index] = target; - } else if (type == int.class) { - arguments[index] = switch (integerIndex++) { - case 0 -> chunkX; - case 1 -> sectionY; - case 2 -> chunkZ; - default -> throw new IllegalStateException("Unexpected integer parameter"); - }; - } else if (type == boolean.class) { - arguments[index] = buildOptions.nativeVoxelTerrainEnabled(); - } else if (type == WorldCollisionBuildOptions.class) { - arguments[index] = buildOptions; - } else { - arguments[index] = null; - } - } - return arguments; - } - - private static void stitchAdjacentVoxelTerrains(@Nonnull PhysicsSpaceBinding space, - @Nonnull Object cache, - @Nonnull Object built) throws Throwable { - Method method = Arrays.stream(WorldVoxelCollisionCache.class.getDeclaredMethods()) - .filter(candidate -> candidate.getName().equals("stitchAdjacentVoxelTerrains")) - .findFirst() - .orElseThrow(); - method.setAccessible(true); - try { - method.invoke(null, space, cache, built); - } catch (InvocationTargetException exception) { - throw exception.getCause(); - } - } - - private static void removeBuiltSectionAfterFailure(@Nonnull PhysicsSpaceBinding space, - @Nonnull Object built, - @Nonnull RuntimeException failure) throws Throwable { - Method method = WorldVoxelCollisionCache.class.getDeclaredMethod("removeBuiltSectionAfterFailure", - PhysicsSpaceBinding.class, - nestedClass("CachedSection"), - RuntimeException.class); - method.setAccessible(true); - try { - method.invoke(null, space, built, failure); - } catch (InvocationTargetException exception) { - throw exception.getCause(); - } - } - - private static Object newSpaceCollisionCache() throws Exception { - Class cacheType = nestedClass("SpaceCollisionCache"); - Constructor constructor = cacheType.getDeclaredConstructor(); - constructor.setAccessible(true); - return constructor.newInstance(); - } - - @SuppressWarnings("unchecked") - private static void putCachedSection(@Nonnull Object cache, @Nonnull Object section) throws Exception { - Method keyMethod = WorldVoxelCollisionCache.class.getDeclaredMethod("packSectionKey", - int.class, - int.class, - int.class); - keyMethod.setAccessible(true); - long key = (long) keyMethod.invoke(null, - intField(section, "chunkX"), - intField(section, "sectionY"), - intField(section, "chunkZ")); - Field sectionsField = cache.getClass().getDeclaredField("sections"); - sectionsField.setAccessible(true); - ((Map) sectionsField.get(cache)).put(key, section); - } - - @SuppressWarnings("unchecked") - private static void putSpaceCache(@Nonnull WorldVoxelCollisionCache worldCache, - @Nonnull SpaceId spaceId, - @Nonnull Object cache) throws Exception { - Field spacesField = WorldVoxelCollisionCache.class.getDeclaredField("spaces"); - spacesField.setAccessible(true); - ((Map) spacesField.get(worldCache)).put(spaceId.value(), cache); - } - - private static Object cachedVoxelNeighbor(@Nonnull RuntimeFixture fixture, - @Nonnull Object cache, - int chunkX, - int sectionY, - int chunkZ) throws Exception { - Object section = newCachedSection(chunkX, sectionY, chunkZ); - markVoxelTerrain(section, createVoxelTerrain(fixture)); - putCachedSection(cache, section); - return section; - } - - private static long createVoxelTerrain(@Nonnull RuntimeFixture fixture) { - return fixture.runtime().createVoxelTerrain(fixture.backendSpaceId(), - 1.0f, - 1.0f, - 1.0f, - new int[] {0, 0, 0}, - 0.0f, - 0.0f, - 0.0f, - 0.75f, - 0.0f, - PhysicsCollisionFilters.TERRAIN, - PhysicsCollisionFilters.ALL); - } - - @SuppressWarnings("unchecked") - private static void markVoxelTerrain(@Nonnull Object section, long backendBodyId) throws Exception { - addBackendBodyId(section, backendBodyId); - setBooleanField(section, "voxelTerrain", true); - setLongField(section, "voxelTerrainBodyId", backendBodyId); - } - - @SuppressWarnings("unchecked") - private static void addBackendBodyId(@Nonnull Object section, long backendBodyId) throws Exception { - Field backendBodyIds = section.getClass().getDeclaredField("backendBodyIds"); - backendBodyIds.setAccessible(true); - ((List) backendBodyIds.get(section)).add(backendBodyId); - } - - @SuppressWarnings("unchecked") - private static long firstBackendBodyId(@Nonnull Object section) throws Exception { - Field backendBodyIds = section.getClass().getDeclaredField("backendBodyIds"); - backendBodyIds.setAccessible(true); - return ((List) backendBodyIds.get(section)).getFirst(); - } - - private static long voxelTerrainBodyId(@Nonnull Object section) throws Exception { - Field field = section.getClass().getDeclaredField("voxelTerrainBodyId"); - field.setAccessible(true); - return field.getLong(section); - } - - private static WorldVoxelCollisionCache.DebugSection debugSection(@Nonnull Object section) throws Exception { - Method method = section.getClass().getDeclaredMethod("debugSection"); - method.setAccessible(true); - return (WorldVoxelCollisionCache.DebugSection) method.invoke(section); - } - - private static int intField(@Nonnull Object target, @Nonnull String name) throws Exception { - Field field = target.getClass().getDeclaredField(name); - field.setAccessible(true); - return field.getInt(target); - } - - private static void setBooleanField(@Nonnull Object target, - @Nonnull String name, - boolean value) throws Exception { - Field field = target.getClass().getDeclaredField(name); - field.setAccessible(true); - field.setBoolean(target, value); - } - - private static void setLongField(@Nonnull Object target, - @Nonnull String name, - long value) throws Exception { - Field field = target.getClass().getDeclaredField(name); - field.setAccessible(true); - field.setLong(target, value); - } - - @Nonnull - private static Class nestedClass(@Nonnull String simpleName) { - return Arrays.stream(WorldVoxelCollisionCache.class.getDeclaredClasses()) - .filter(candidate -> candidate.getSimpleName().equals(simpleName)) - .findFirst() - .orElseThrow(); - } - - private static RuntimeFixture runtimeFixture(@Nonnull String backendId, boolean voxelTerrain) { - BackendId id = new BackendId(backendId); - FakePhysicsBackendRuntimeProvider provider = - new FakePhysicsBackendRuntimeProvider(id, false, voxelTerrain); - FakePhysicsBackendRuntime runtime = (FakePhysicsBackendRuntime) provider.createRuntime(); - SpaceId spaceId = new SpaceId(42); - int backendSpaceId = runtime.createSpace(spaceId); - return new RuntimeFixture(provider, - new PhysicsSpaceBinding(id, - spaceId, - new BackendSpaceHandle(backendSpaceId), - runtime), - runtime, - backendSpaceId); - } - - private record RuntimeFixture(@Nonnull FakePhysicsBackendRuntimeProvider provider, - @Nonnull PhysicsSpaceBinding binding, - @Nonnull FakePhysicsBackendRuntime runtime, - int backendSpaceId) { - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionPerfReportCommandTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionPerfReportCommandTest.java deleted file mode 100644 index 063a8253..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/commands/WorldCollisionPerfReportCommandTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.commands; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.PhysicsStepPhaseStats; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import org.junit.jupiter.api.Test; - -class WorldCollisionPerfReportCommandTest { - - @Test - void preStepDrainSummaryReportsAverageLatestAndMaxBackpressure() { - PhysicsRuntimeProfilingResource profiling = new PhysicsRuntimeProfilingResource(); - profiling.recordStep(1, - 1, - 20L, - 0, - 0, - 0L, - 0L, - 0L, - 1_000_000_000L, - PhysicsStepPhaseStats.unavailable(), - 2, - 4_000_000L, - 1); - profiling.recordStep(1, - 1, - 10L, - 0, - 0, - 0L, - 0L, - 0L, - 1_010_000_000L, - PhysicsStepPhaseStats.unavailable(), - 4, - 8_000_000L, - 3); - - assertEquals("Physics pre-step drain avg completedStep drained/runMs/lateBacklog=3.0/6.000/2.0 " - + "latest drained/lateBacklog=4/3 max drained/lateBacklog=4/3", - WorldCollisionPerfReportCommand.formatPreStepDrainSummary(profiling.getCumulativeStep(), - profiling.getLatestStep())); - } - - @Test - void preStepDrainSummaryRequiresCompletedStepSamples() { - PhysicsRuntimeProfilingResource profiling = new PhysicsRuntimeProfilingResource(); - - profiling.recordStepScheduling(0.05f, 0.05f, 0.0f, 0.0f, false); - - assertFalse(WorldCollisionPerfReportCommand.hasCompletedStepSamples( - profiling.getCumulativeStep())); - - profiling.recordStep(1, - 1, - 20L, - 0, - 0, - 0L, - 0L, - 0L, - 1_000_000_000L, - PhysicsStepPhaseStats.unavailable(), - 1, - 2_000_000L, - 0); - - assertTrue(WorldCollisionPerfReportCommand.hasCompletedStepSamples( - profiling.getCumulativeStep())); - } - - @Test - void preStepDrainSummaryUsesLatestCompletedStepAfterSkippedPendingTick() { - PhysicsRuntimeProfilingResource profiling = new PhysicsRuntimeProfilingResource(); - profiling.recordStep(1, - 1, - 20L, - 0, - 0, - 0L, - 0L, - 0L, - 1_000_000_000L, - PhysicsStepPhaseStats.unavailable(), - 4, - 8_000_000L, - 3); - - profiling.recordStepSkippedPending(2_000_000L); - - assertEquals("Physics pre-step drain avg completedStep drained/runMs/lateBacklog=4.0/8.000/3.0 " - + "latest drained/lateBacklog=4/3 max drained/lateBacklog=4/3", - WorldCollisionPerfReportCommand.formatPreStepDrainSummary(profiling.getCumulativeStep(), - profiling.getLatestCompletedStep())); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsChunkBoundarySystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsChunkBoundarySystemTest.java deleted file mode 100644 index 3a089dd9..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsChunkBoundarySystemTest.java +++ /dev/null @@ -1,432 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.hypixel.hytale.math.util.ChunkUtil; -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.PhysicsChunkBoundaryRuntime; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.PhysicsChunkBoundaryRuntime.ChunkBoundarySafeState; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems.PhysicsChunkBoundarySystem; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Quaternionf; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class PhysicsChunkBoundarySystemTest { - - @Test - void recordSafePoseUsesSnapshotPoseWithoutReadingBody() { - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - RigidBodyKey bodyId = RigidBodyKey.random(); - CountingBody body = new CountingBody(); - Quaternionf rotation = new Quaternionf().rotateY(0.5f); - PhysicsBodySnapshot snapshot = snapshot( - new Vector3f(3.0f, 4.0f, 5.0f), - rotation, - new Vector3f(), - new Vector3f(), - PhysicsBodyType.DYNAMIC); - - PhysicsChunkBoundarySystem.recordSafePose(bodyId, snapshot, resource); - - ChunkBoundarySafeState safeState = - resource.getChunkBoundarySafeState(bodyId); - assertNotNull(safeState); - assertEquals(3.0f, safeState.getPosition().x); - assertEquals(4.0f, safeState.getPosition().y); - assertEquals(5.0f, safeState.getPosition().z); - assertEquals(rotation.x, safeState.getRotation().x); - assertEquals(rotation.y, safeState.getRotation().y); - assertEquals(rotation.z, safeState.getRotation().z); - assertEquals(rotation.w, safeState.getRotation().w); - assertEquals(0, body.liveGetterCalls()); - } - - @Test - void chunkFootprintUsesBoxExtentsWhenCenterRemainsInLoadedChunk() { - PhysicsBodySnapshot snapshot = snapshot( - new Vector3f(31.75f, 0.0f, 8.0f), - new Quaternionf(), - new Vector3f(), - new Vector3f(), - PhysicsBodyType.DYNAMIC, - new Vector3f(0.5f, 0.5f, 0.5f)); - - assertArrayEquals(new long[] { - ChunkUtil.indexChunk(0, 0), - ChunkUtil.indexChunk(1, 0) - }, - PhysicsChunkBoundarySystem.chunkIndices(snapshot)); - } - - @Test - void chunkFootprintUsesRotatedBoxExtents() { - PhysicsBodySnapshot snapshot = snapshot( - new Vector3f(30.25f, 0.0f, 8.0f), - new Quaternionf().rotateY((float) (Math.PI / 2.0)), - new Vector3f(), - new Vector3f(), - PhysicsBodyType.DYNAMIC, - new Vector3f(0.25f, 0.5f, 2.0f)); - - assertArrayEquals(new long[] { - ChunkUtil.indexChunk(0, 0), - ChunkUtil.indexChunk(1, 0) - }, - PhysicsChunkBoundarySystem.chunkIndices(snapshot)); - } - - @Test - void pauseBodyUsesSnapshotVelocityAndTypeWithoutReadingBody() { - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - RigidBodyKey bodyId = RigidBodyKey.random(); - CountingBody body = new CountingBody(); - Quaternionf safeRotation = new Quaternionf().rotateX(0.25f); - resource.updateChunkBoundarySafeState(bodyId, - new Vector3f(8.0f, 9.0f, 10.0f), - safeRotation); - Vector3f linearVelocity = new Vector3f(1.0f, 2.0f, 3.0f); - Vector3f angularVelocity = new Vector3f(4.0f, 5.0f, 6.0f); - PhysicsBodySnapshot snapshot = snapshot( - new Vector3f(24.0f, 0.0f, 24.0f), - new Quaternionf(), - linearVelocity, - angularVelocity, - PhysicsBodyType.DYNAMIC); - - resource.pauseChunkBoundaryBody(bodyId, 42L, snapshot); - - PhysicsChunkBoundaryRuntime.ChunkBoundaryPauseState pauseState = - resource.getChunkBoundaryPauseState(bodyId); - assertNotNull(pauseState); - assertEquals(42L, pauseState.getTargetChunkIndex()); - assertEquals(PhysicsBodyType.DYNAMIC, pauseState.getOriginalBodyType()); - assertEquals(linearVelocity, pauseState.getLinearVelocity()); - assertEquals(angularVelocity, pauseState.getAngularVelocity()); - assertEquals(0, body.liveGetterCalls()); - } - - @Nonnull - private static PhysicsBodySnapshot snapshot(@Nonnull Vector3f position, - @Nonnull Quaternionf rotation, - @Nonnull Vector3f linearVelocity, - @Nonnull Vector3f angularVelocity, - @Nonnull PhysicsBodyType bodyType) { - return snapshot(position, - rotation, - linearVelocity, - angularVelocity, - bodyType, - new Vector3f(0.5f)); - } - - @Nonnull - private static PhysicsBodySnapshot snapshot(@Nonnull Vector3f position, - @Nonnull Quaternionf rotation, - @Nonnull Vector3f linearVelocity, - @Nonnull Vector3f angularVelocity, - @Nonnull PhysicsBodyType bodyType, - @Nonnull Vector3f boxHalfExtents) { - return new PhysicsBodySnapshot(position, - rotation, - linearVelocity, - angularVelocity, - bodyType, - false, - false, - 0.0f, - ShapeType.BOX, - boxHalfExtents, - -1.0f, - -1.0f, - PhysicsAxis.Y); - } - - private static final class CountingBody implements PhysicsBody { - - private final Vector3f position = new Vector3f(); - private final Quaternionf rotation = new Quaternionf(); - private final Vector3f linearVelocity = new Vector3f(); - private final Vector3f angularVelocity = new Vector3f(); - private PhysicsBodyType bodyType = PhysicsBodyType.DYNAMIC; - private boolean forcesCleared; - private int liveGetterCalls; - - private int liveGetterCalls() { - return liveGetterCalls; - } - - @Override - public void setPosition(float x, float y, float z) { - position.set(x, y, z); - } - - @Override - public void setPosition(@Nonnull Vector3f pos) { - position.set(pos); - } - - @Nonnull - @Override - public Vector3f getPosition() { - liveGetterCalls++; - return new Vector3f(position); - } - - @Override - public void setRotation(float x, float y, float z, float w) { - rotation.set(x, y, z, w); - } - - @Override - public void setRotation(@Nonnull Quaternionf rot) { - rotation.set(rot); - } - - @Nonnull - @Override - public Quaternionf getRotation() { - liveGetterCalls++; - return new Quaternionf(rotation); - } - - @Override - public void setRestitution(float restitution) { - } - - @Override - public float getRestitution() { - return 0.0f; - } - - @Override - public void setFriction(float friction) { - } - - @Override - public float getFriction() { - return 0.0f; - } - - @Nonnull - @Override - public PhysicsBodyType getBodyType() { - liveGetterCalls++; - return bodyType; - } - - @Override - public void setBodyType(@Nonnull PhysicsBodyType bodyType) { - this.bodyType = bodyType; - } - - @Override - public boolean isStatic() { - liveGetterCalls++; - return bodyType == PhysicsBodyType.STATIC; - } - - @Override - public boolean isKinematic() { - liveGetterCalls++; - return bodyType == PhysicsBodyType.KINEMATIC; - } - - @Override - public void setKinematic(boolean kinematic) { - bodyType = kinematic ? PhysicsBodyType.KINEMATIC : PhysicsBodyType.DYNAMIC; - } - - @Override - public void activate() { - } - - @Override - public boolean isActive() { - return true; - } - - @Override - public boolean isSleeping() { - liveGetterCalls++; - return false; - } - - @Override - public void sleep() { - } - - @Override - public float getMass() { - return 1.0f; - } - - @Override - public void setMass(float mass) { - } - - @Nonnull - @Override - public Vector3f getLinearVelocity() { - liveGetterCalls++; - return new Vector3f(linearVelocity); - } - - @Override - public void setLinearVelocity(@Nonnull Vector3f vel) { - linearVelocity.set(vel); - } - - @Override - public void setLinearVelocity(float x, float y, float z) { - linearVelocity.set(x, y, z); - } - - @Nonnull - @Override - public Vector3f getAngularVelocity() { - liveGetterCalls++; - return new Vector3f(angularVelocity); - } - - @Override - public void setAngularVelocity(@Nonnull Vector3f vel) { - angularVelocity.set(vel); - } - - @Override - public void setAngularVelocity(float x, float y, float z) { - angularVelocity.set(x, y, z); - } - - @Override - public float getLinearDamping() { - return 0.0f; - } - - @Override - public void setLinearDamping(float damping) { - } - - @Override - public float getAngularDamping() { - return 0.0f; - } - - @Override - public void setAngularDamping(float damping) { - } - - @Override - public void applyCentralForce(@Nonnull Vector3f force) { - } - - @Override - public void applyCentralForce(float x, float y, float z) { - } - - @Override - public void applyForce(@Nonnull Vector3f force, @Nonnull Vector3f offset) { - } - - @Override - public void applyCentralImpulse(@Nonnull Vector3f impulse) { - } - - @Override - public void applyCentralImpulse(float x, float y, float z) { - } - - @Override - public void applyImpulse(@Nonnull Vector3f impulse, @Nonnull Vector3f offset) { - } - - @Override - public void applyTorque(@Nonnull Vector3f torque) { - } - - @Override - public void applyTorqueImpulse(@Nonnull Vector3f torqueImpulse) { - } - - @Override - public void clearForces() { - forcesCleared = true; - } - - @Override - public boolean isSensor() { - return false; - } - - @Override - public void setSensor(boolean sensor) { - } - - @Override - public int getCollisionGroup() { - return 0; - } - - @Override - public int getCollisionMask() { - return 0; - } - - @Override - public void setCollisionFilter(int group, int mask) { - } - - @Override - public boolean isContinuousCollisionEnabled() { - return false; - } - - @Override - public void setContinuousCollisionEnabled(boolean enabled) { - } - - @Nonnull - @Override - public ShapeType getShapeType() { - return ShapeType.BOX; - } - - @Nullable - @Override - public Vector3f getBoxHalfExtents() { - return null; - } - - @Override - public float getSphereRadius() { - return 0.0f; - } - - @Override - public float getHalfHeight() { - return 0.0f; - } - - @Nonnull - @Override - public PhysicsAxis getShapeAxis() { - return PhysicsAxis.Y; - } - - @Override - public float getCenterOfMassOffsetY() { - return 0.0f; - } - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsCollisionLodSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsCollisionLodSystemTest.java deleted file mode 100644 index 512d7f94..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsCollisionLodSystemTest.java +++ /dev/null @@ -1,200 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems.PhysicsCollisionLodSystem; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems.PhysicsCollisionLodSystem.CollisionLodState; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems.PhysicsCollisionLodSystem.CollisionLodTier; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems.PhysicsCollisionLodSystem.CollisionLodUpdate; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import java.util.ArrayList; -import java.util.List; -import org.joml.Quaternionf; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class PhysicsCollisionLodSystemTest { - - @Test - void resolvesDistanceTiersAroundInterest() { - PhysicsSpaceSettings settings = testSettings(); - List interests = interestsAtOrigin(); - - assertEquals(CollisionLodTier.NEAR_FULL, - PhysicsCollisionLodSystem.resolveTier(settings, - null, - new Vector3f(9.0f, 0.0f, 0.0f), - interests)); - assertEquals(CollisionLodTier.MID_TERRAIN, - PhysicsCollisionLodSystem.resolveTier(settings, - null, - new Vector3f(20.0f, 0.0f, 0.0f), - interests)); - assertEquals(CollisionLodTier.FAR_SLEEPING, - PhysicsCollisionLodSystem.resolveTier(settings, - null, - new Vector3f(31.0f, 0.0f, 0.0f), - interests)); - } - - @Test - void keepsPreviousTierInsideHysteresis() { - PhysicsSpaceSettings settings = testSettings(); - List interests = interestsAtOrigin(); - - assertEquals(CollisionLodTier.NEAR_FULL, - PhysicsCollisionLodSystem.resolveTier(settings, - CollisionLodTier.NEAR_FULL, - new Vector3f(14.0f, 0.0f, 0.0f), - interests)); - assertEquals(CollisionLodTier.MID_TERRAIN, - PhysicsCollisionLodSystem.resolveTier(settings, - CollisionLodTier.NEAR_FULL, - new Vector3f(16.0f, 0.0f, 0.0f), - interests)); - assertEquals(CollisionLodTier.MID_TERRAIN, - PhysicsCollisionLodSystem.resolveTier(settings, - CollisionLodTier.MID_TERRAIN, - new Vector3f(34.0f, 0.0f, 0.0f), - interests)); - assertEquals(CollisionLodTier.FAR_SLEEPING, - PhysicsCollisionLodSystem.resolveTier(settings, - CollisionLodTier.MID_TERRAIN, - new Vector3f(36.0f, 0.0f, 0.0f), - interests)); - } - - @Test - void noInterestsResolveToFarTier() { - PhysicsSpaceSettings settings = testSettings(); - - assertEquals(CollisionLodTier.FAR_SLEEPING, - PhysicsCollisionLodSystem.resolveTier(settings, - CollisionLodTier.NEAR_FULL, - new Vector3f(), - List.of())); - } - - @Test - void persistentBodiesAreNotCollisionLodCandidates() { - assertFalse(PhysicsCollisionLodSystem.isCollisionLodCandidate(snapshot( - PhysicsBodyType.DYNAMIC, - false), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT)); - } - - @Test - void onlyRuntimeDynamicBodiesAreCollisionLodCandidates() { - assertTrue(PhysicsCollisionLodSystem.isCollisionLodCandidate(snapshot( - PhysicsBodyType.DYNAMIC, - false), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY)); - assertFalse(PhysicsCollisionLodSystem.isCollisionLodCandidate(snapshot( - PhysicsBodyType.STATIC, - false), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY)); - assertFalse(PhysicsCollisionLodSystem.isCollisionLodCandidate(snapshot( - PhysicsBodyType.DYNAMIC, - true), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY)); - } - - @Test - void tierStateCommitsAfterSuccessfulOwnerMutation() { - CollisionLodState state = new CollisionLodState(); - SpaceId spaceId = new SpaceId(1); - RigidBodyKey bodyId = RigidBodyKey.random(); - List updates = new ArrayList<>(); - - state.recordTier(spaceId, bodyId, CollisionLodTier.MID_TERRAIN, updates); - - assertNull(state.tier(bodyId)); - state.trackPendingMutation(PhysicsMutationHandle.completed("test", null), updates); - state.refreshPendingMutation(); - - assertEquals(CollisionLodTier.MID_TERRAIN, state.tier(bodyId)); - assertFalse(state.hasPendingMutation()); - } - - @Test - void tierStateRetriesAfterFailedOwnerMutation() { - CollisionLodState state = new CollisionLodState(); - SpaceId spaceId = new SpaceId(1); - RigidBodyKey bodyId = RigidBodyKey.random(); - List updates = new ArrayList<>(); - - assertTrue(state.shouldRefresh(spaceId, 20, 1L)); - state.recordTier(spaceId, bodyId, CollisionLodTier.MID_TERRAIN, updates); - state.trackPendingMutation(PhysicsMutationHandle.failed("test", - null, - new IllegalStateException("boom")), - updates); - state.refreshPendingMutation(); - - assertNull(state.tier(bodyId)); - assertTrue(state.shouldRefresh(spaceId, 20, 2L)); - } - - @Test - void restoreClearsTierStateAfterSuccessfulOwnerMutation() { - CollisionLodState state = new CollisionLodState(); - SpaceId spaceId = new SpaceId(1); - RigidBodyKey bodyId = RigidBodyKey.random(); - List updates = new ArrayList<>(); - - state.recordTier(spaceId, bodyId, CollisionLodTier.MID_TERRAIN, updates); - state.trackPendingMutation(PhysicsMutationHandle.completed("test", null), updates); - state.refreshPendingMutation(); - - updates = new ArrayList<>(); - state.recordRestore(spaceId, bodyId, updates); - state.trackPendingMutation(PhysicsMutationHandle.completed("test", null), updates); - state.refreshPendingMutation(); - - assertNull(state.tier(bodyId)); - } - - private static PhysicsSpaceSettings testSettings() { - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - settings.getCollisionLodSettings().setCollisionLodRadii(10, 30); - settings.getCollisionLodSettings().setCollisionLodHysteresis(5); - return settings; - } - - private static List interestsAtOrigin() { - return List.of(new PhysicsVisualRuntime.VisualInterest(new Vector3f(), null)); - } - - private static PhysicsBodySnapshot snapshot(PhysicsBodyType bodyType, boolean sensor) { - return new PhysicsBodySnapshot(new Vector3f(), - new Quaternionf(), - new Vector3f(), - new Vector3f(), - bodyType, - false, - sensor, - 0.0f, - ShapeType.BOX, - new Vector3f(0.5f), - -1.0f, - -1.0f, - PhysicsAxis.Y); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsWorldCollisionStreamingSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsWorldCollisionStreamingSystemTest.java deleted file mode 100644 index dbef59f8..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/modules/worldcollision/systems/PhysicsWorldCollisionStreamingSystemTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldVoxelCollisionCache; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.profiling.WorldCollisionProfilingResource; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.atomic.AtomicBoolean; -import org.junit.jupiter.api.Test; - -class PhysicsWorldCollisionStreamingSystemTest { - - @Test - void rejectedStreamingApplyClearsPendingGateAndRecordsSkip() { - WorldVoxelCollisionCache cache = new WorldVoxelCollisionCache(); - WorldCollisionProfilingResource.Snapshot snapshot = - new WorldCollisionProfilingResource.Snapshot(); - - assertTrue(cache.tryBeginStreamingApply()); - - PhysicsWorldCollisionStreamingSystem.finishRejectedStreamingApply(cache, snapshot); - - assertFalse(cache.isStreamingApplyPending()); - assertEquals(1, snapshot.getTerrainApplySkippedPending()); - assertEquals(0, snapshot.getTerrainApplyQueued()); - } - - @Test - void rejectedQueuedStreamingApplyClearsPendingGateAndKeepsSnapshotOpen() { - WorldVoxelCollisionCache cache = new WorldVoxelCollisionCache(); - WorldCollisionProfilingResource.Snapshot snapshot = - new WorldCollisionProfilingResource.Snapshot(); - - assertTrue(cache.tryBeginStreamingApply()); - - boolean queued = PhysicsWorldCollisionStreamingSystem.tryQueueStreamingApply(cache, - new WorldCollisionProfilingResource(), - snapshot, - System.nanoTime(), - _ -> { - throw new RejectedExecutionException("forced enqueue rejection"); - }); - - assertFalse(queued); - assertFalse(cache.isStreamingApplyPending()); - assertEquals(0, snapshot.getTerrainApplyQueued()); - assertEquals(1, snapshot.getTerrainApplySkippedPending()); - } - - @Test - void failedHandleStreamingApplyClearsPendingGateAndRecordsSkip() { - WorldVoxelCollisionCache cache = new WorldVoxelCollisionCache(); - WorldCollisionProfilingResource.Snapshot snapshot = - new WorldCollisionProfilingResource.Snapshot(); - - assertTrue(cache.tryBeginStreamingApply()); - - boolean queued = PhysicsWorldCollisionStreamingSystem.tryQueueStreamingApply(cache, - new WorldCollisionProfilingResource(), - snapshot, - System.nanoTime(), - _ -> PhysicsMutationHandle.failed("stream world collision terrain apply", - null, - new RejectedExecutionException("forced handle rejection"))); - - assertFalse(queued); - assertFalse(cache.isStreamingApplyPending()); - assertEquals(0, snapshot.getTerrainApplyQueued()); - assertEquals(1, snapshot.getTerrainApplySkippedPending()); - } - - @Test - void queuedStreamingApplyFinishesGateAndProfilingOnce() { - WorldVoxelCollisionCache cache = new WorldVoxelCollisionCache(); - WorldCollisionProfilingResource profiling = new WorldCollisionProfilingResource(); - WorldCollisionProfilingResource.Snapshot snapshot = profiling.beginTick(); - AtomicBoolean finished = new AtomicBoolean(); - - assertTrue(cache.tryBeginStreamingApply()); - - PhysicsWorldCollisionStreamingSystem.finishQueuedStreamingApply(cache, - profiling, - snapshot, - System.nanoTime(), - finished); - PhysicsWorldCollisionStreamingSystem.finishQueuedStreamingApply(cache, - profiling, - snapshot, - System.nanoTime(), - finished); - - assertFalse(cache.isStreamingApplyPending()); - assertEquals(1, profiling.getCumulativeSnapshot().getTickSamples()); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsCodecValidationTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsCodecValidationTest.java deleted file mode 100644 index 724d96fe..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsCodecValidationTest.java +++ /dev/null @@ -1,571 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.persistence; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.hypixel.hytale.codec.ExtraInfo; -import com.hypixel.hytale.codec.util.RawJsonReader; -import com.hypixel.hytale.server.core.util.BsonUtil; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsJointType; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodySnapshots; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistration; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsBackendExtensionId; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsEventCollectionMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepSchedulingMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; -import javax.annotation.Nonnull; -import org.bson.BsonBinary; -import org.bson.BsonDocument; -import org.bson.BsonDouble; -import org.bson.BsonInt32; -import org.bson.BsonNull; -import org.bson.BsonString; -import org.joml.Quaternionf; -import org.joml.Vector3f; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; - -class PersistentPhysicsCodecValidationTest { - - private static final AtomicInteger BACKEND_COUNTER = new AtomicInteger(); - - @Test - void runtimeRestoreGenerationChangesAcrossRestoreTransitions() { - PersistentPhysicsWorldResource resource = new PersistentPhysicsWorldResource(); - - long initialGeneration = resource.runtimeRestoreGeneration(); - resource.markRuntimeRestorePending(); - long pendingGeneration = resource.runtimeRestoreGeneration(); - resource.clearRuntimeRestorePending(); - - assertTrue(pendingGeneration > initialGeneration); - assertTrue(resource.runtimeRestoreGeneration() > pendingGeneration); - } - - @Test - void spaceStateCodecValidatorChecksCrossFieldSettingsAfterDecode() { - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - settings.getVisualSyncSettings().setVisualSyncRadii(64, 128); - SpaceFixture fixture = spaceFixture("test:space-codec-validation-", settings); - BsonDocument encoded = encodeSpace(PersistentPhysicsSpaceState.from(fixture.binding(), settings)); - encoded.put("VisualFullSyncRadius", new BsonInt32(128)); - encoded.put("VisualMaxSyncRadius", new BsonInt32(64)); - - assertValidationFails( - () -> PersistentPhysicsSpaceState.CODEC.decode(encoded, new ExtraInfo()), - "Visual full sync radius cannot exceed visual max sync radius"); - } - - @Test - void worldResourceCodecValidatorChecksStepBudgetAfterDecode() { - BsonDocument encoded = encodeWorld(new PersistentPhysicsWorldResource()); - encoded.put("SimulationSteps", new BsonInt32(0)); - - assertValidationFails( - () -> PersistentPhysicsWorldResource.CODEC.decode(encoded, new ExtraInfo()), - "Must be greater than or equal to 1"); - } - - @Test - void spaceStateFieldValidatorsRejectNullOptionalEnums() { - BsonDocument encoded = encodedSpaceState(); - encoded.put("WorldCollisionMode", BsonNull.VALUE); - - assertValidationFails( - () -> PersistentPhysicsSpaceState.CODEC.decode(encoded, new ExtraInfo()), - "Can't be null"); - } - - @Test - void spaceStateFieldValidatorsRejectBlankDetachedVisualBlockType() { - BsonDocument encoded = encodedSpaceState(); - encoded.put("DetachedVisualBlockType", new BsonString(" ")); - - assertValidationFails( - () -> PersistentPhysicsSpaceState.CODEC.decode(encoded, new ExtraInfo()), - "Persisted detached visual block type cannot be blank"); - } - - @Test - void worldResourceFieldValidatorsRejectUnknownStepMode() { - BsonDocument encoded = encodeWorld(new PersistentPhysicsWorldResource()); - encoded.put("StepMode", new BsonString("bogus")); - - assertValidationFails( - () -> PersistentPhysicsWorldResource.CODEC.decode(encoded, new ExtraInfo()), - "Persistent physics step mode is unknown: bogus"); - } - - @Test - void worldResourceFieldValidatorsRejectUnknownStepSchedulingMode() { - BsonDocument encoded = encodeWorld(new PersistentPhysicsWorldResource()); - encoded.put("StepSchedulingMode", new BsonString("bogus")); - - assertValidationFails( - () -> PersistentPhysicsWorldResource.CODEC.decode(encoded, new ExtraInfo()), - "Persistent physics step scheduling mode is unknown: bogus"); - } - - @Test - void worldResourceFieldValidatorsRejectUnknownEventCollectionMode() { - BsonDocument encoded = encodeWorld(new PersistentPhysicsWorldResource()); - encoded.put("EventCollectionMode", new BsonString("bogus")); - - assertValidationFails( - () -> PersistentPhysicsWorldResource.CODEC.decode(encoded, new ExtraInfo()), - "Persistent physics event collection mode is unknown: bogus"); - } - - @Test - void worldResourceCodecPreservesStepSchedulingMode() { - PersistentPhysicsWorldResource resource = new PersistentPhysicsWorldResource(); - PhysicsWorldSettings settings = resource.getWorldSettings(); - settings.setStepSchedulingMode(PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT); - resource.setWorldSettings(settings); - - BsonDocument encoded = encodeWorld(resource); - PersistentPhysicsWorldResource decoded = PersistentPhysicsWorldResource.CODEC - .decode(encoded, new ExtraInfo()); - - assertEquals("accumulate_pending_dt", - encoded.getString("StepSchedulingMode").getValue()); - Assertions.assertNotNull(decoded); - assertEquals(PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT, - decoded.getWorldSettings().getStepSchedulingMode()); - } - - @Test - void worldResourceCodecPreservesEventCollectionMode() { - PersistentPhysicsWorldResource resource = new PersistentPhysicsWorldResource(); - PhysicsWorldSettings settings = resource.getWorldSettings(); - settings.setEventCollectionMode(PhysicsEventCollectionMode.CONTACTS); - resource.setWorldSettings(settings); - - BsonDocument encoded = encodeWorld(resource); - PersistentPhysicsWorldResource decoded = PersistentPhysicsWorldResource.CODEC - .decode(encoded, new ExtraInfo()); - - assertEquals("contacts", encoded.getString("EventCollectionMode").getValue()); - Assertions.assertNotNull(decoded); - assertEquals(PhysicsEventCollectionMode.CONTACTS, - decoded.getWorldSettings().getEventCollectionMode()); - } - - @Test - void worldResourceCodecWritesStateBlocksWithoutFlatBodiesAndJoints() { - PersistentPhysicsWorldResource resource = new PersistentPhysicsWorldResource(); - PersistentPhysicsBodyState body = persistentBodyState(); - PersistentPhysicsJointState joint = persistentJointState(body.getSpaceId()); - resource.setBodies(new PersistentPhysicsBodyState[] { body }); - resource.setJoints(new PersistentPhysicsJointState[] { joint }); - - BsonDocument encoded = encodeWorld(resource); - - assertEquals(PersistentPhysicsWorldResource.CURRENT_SCHEMA_VERSION, - encoded.getInt32("SchemaVersion").getValue()); - assertFalse(encoded.containsKey("Bodies")); - assertFalse(encoded.containsKey("Joints")); - assertFalse(encoded.getArray("BodyBlocks").isEmpty()); - assertFalse(encoded.getArray("JointBlocks").isEmpty()); - - PersistentPhysicsWorldResource decoded = PersistentPhysicsWorldResource.CODEC - .decode(encoded, new ExtraInfo()); - - Assertions.assertNotNull(decoded); - assertEquals(1, decoded.getBodyCount()); - assertEquals(1, decoded.getJointCount()); - assertEquals(body.getBodyIdValue(), decoded.getBodies()[0].getBodyIdValue()); - assertEquals(joint.key(), decoded.getJoints()[0].key()); - } - - @Test - void worldResourceCodecRejectsCorruptStateBlockPayload() { - PersistentPhysicsWorldResource resource = new PersistentPhysicsWorldResource(); - resource.setBodies(new PersistentPhysicsBodyState[] { persistentBodyState() }); - BsonDocument encoded = encodeWorld(resource); - BsonDocument block = encoded.getArray("BodyBlocks").getFirst().asDocument(); - byte[] payload = block.getBinary("Payload").getData(); - payload[payload.length / 2] ^= 0x01; - block.put("Payload", new BsonBinary(payload)); - - assertThrows(RuntimeException.class, - () -> PersistentPhysicsWorldResource.CODEC.decode(encoded, new ExtraInfo())); - } - - @Test - void worldResourceCodecRejectsOversizedStateBlockBeforeInflating() { - PersistentPhysicsWorldResource resource = new PersistentPhysicsWorldResource(); - resource.setBodies(new PersistentPhysicsBodyState[] { persistentBodyState() }); - BsonDocument encoded = encodeWorld(resource); - BsonDocument block = encoded.getArray("BodyBlocks").getFirst().asDocument(); - block.put("UncompressedBytes", new BsonInt32(Integer.MAX_VALUE)); - - assertValidationFails( - () -> PersistentPhysicsStateBlock.CODEC.decode(block, new ExtraInfo()), - "Persistent physics state block uncompressed size exceeds"); - } - - @Test - void bodyStateCodecRejectsInvalidSpaceIdWithoutRuntimeFallback() { - BsonDocument encoded = encodeBody(persistentBodyState()); - encoded.put("SpaceId", new BsonInt32(0)); - - assertValidationFails( - () -> PersistentPhysicsBodyState.CODEC.decode(encoded, new ExtraInfo()), - "Must be greater than or equal to 1"); - } - - @Test - void bodyStateCaptureRejectsMissingSpaceId() { - PersistentPhysicsBodyState state = new PersistentPhysicsBodyState(); - - assertThrows(NullPointerException.class, () -> state.updateFromSnapshot(boxSnapshot(0.0f), null)); - } - - @Test - void bodyStateCaptureRejectsInvalidSpaceId() { - PersistentPhysicsBodyState state = new PersistentPhysicsBodyState(); - - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> state.updateFromSnapshot(boxSnapshot(0.0f), new SpaceId(0))); - - assertEquals("Persistent body state requires a positive explicit space id", - exception.getMessage()); - } - - @Test - void bodyStateCodecRejectsInvalidMassInsteadOfDefaulting() { - BsonDocument encoded = encodeBody(persistentBodyState()); - encoded.put("Mass", new BsonDouble(Double.NaN)); - - assertValidationFails( - () -> PersistentPhysicsBodyState.CODEC.decode(encoded, new ExtraInfo()), - "Persisted body mass must be finite and >= 0"); - } - - @Test - void bodyStateCodecRejectsZeroQuaternionInsteadOfRestoringInvalidRotation() { - BsonDocument encoded = encodeBody(persistentBodyState()); - BsonDocument rotation = encoded.getDocument("Rotation"); - rotation.put("X", new BsonDouble(0.0)); - rotation.put("Y", new BsonDouble(0.0)); - rotation.put("Z", new BsonDouble(0.0)); - rotation.put("W", new BsonDouble(0.0)); - - assertValidationFails( - () -> PersistentPhysicsBodyState.CODEC.decode(encoded, new ExtraInfo()), - "Persisted quaternion must be finite and non-zero"); - } - - @Test - void bodyStateCodecNormalizesNonUnitQuaternion() { - BsonDocument encoded = encodeBody(persistentBodyState()); - BsonDocument rotation = encoded.getDocument("Rotation"); - rotation.put("X", new BsonDouble(0.0)); - rotation.put("Y", new BsonDouble(0.0)); - rotation.put("Z", new BsonDouble(0.0)); - rotation.put("W", new BsonDouble(2.0)); - - PersistentPhysicsBodyState decoded = - PersistentPhysicsBodyState.CODEC.decode(encoded, new ExtraInfo()); - - Assertions.assertNotNull(decoded); - assertEquals(1.0f, decoded.getRotation().lengthSquared(), 0.0001f); - assertEquals(1.0f, decoded.getRotation().w, 0.0001f); - } - - @Test - void bodyStateRestoreNormalizesQuaternionBeforeBackendHandoff() { - BsonDocument encoded = encodeBody(persistentBodyState()); - BsonDocument rotation = encoded.getDocument("Rotation"); - rotation.put("X", new BsonDouble(0.0)); - rotation.put("Y", new BsonDouble(0.0)); - rotation.put("Z", new BsonDouble(0.0)); - rotation.put("W", new BsonDouble(2.0)); - PersistentPhysicsBodyState decoded = - PersistentPhysicsBodyState.CODEC.decode(encoded, new ExtraInfo()); - SpaceFixture restore = spaceFixture("test:quaternion-restore-", PhysicsSpaceSettings.defaults()); - - Assertions.assertNotNull(decoded); - BackendBodyHandle restoredBodyHandle = decoded.createBackendBody(restore.binding()); - decoded.applyToBody(restore.binding(), restoredBodyHandle); - PhysicsBodySnapshot restored = PhysicsBodySnapshots.read(restore.binding(), restoredBodyHandle.value()); - - Assertions.assertNotNull(restored); - assertEquals(1.0f, restored.rotation().lengthSquared(), 0.0001f); - assertEquals(1.0f, restored.rotationW(), 0.0001f); - } - - @Test - void bodyStateCodecRejectsUnsupportedPersistentShape() { - BsonDocument encoded = encodeBody(persistentBodyState()); - encoded.put("ShapeType", new BsonString("Voxels")); - - assertValidationFails( - () -> PersistentPhysicsBodyState.CODEC.decode(encoded, new ExtraInfo()), - "Persisted body shape is unsupported: VOXELS"); - } - - @Test - void bodyStateCodecDoesNotWritePlaneGroundY() { - BsonDocument encoded = encodeBody(persistentPlaneBodyState(12.0f)); - - assertFalse(encoded.containsKey("PlaneGroundY")); - } - - @Test - void worldResourceCodecRestoresPlaneHeightFromPositionY() { - PersistentPhysicsBodyState state = persistentPlaneBodyState(12.0f); - BsonDocument encodedBody = encodeBody(state); - assertEquals(12.0, encodedBody.getDocument("Position").getDouble("Y").doubleValue(), 0.0001); - PersistentPhysicsWorldResource resource = new PersistentPhysicsWorldResource(); - resource.setBodies(new PersistentPhysicsBodyState[] { state }); - - PersistentPhysicsWorldResource decoded = PersistentPhysicsWorldResource.CODEC - .decode(encodeWorld(resource), new ExtraInfo()); - SpaceFixture restore = spaceFixture("test:plane-restore-", PhysicsSpaceSettings.defaults()); - - Assertions.assertNotNull(decoded); - assertEquals(1, decoded.getBodyCount()); - PersistentPhysicsBodyState decodedBody = decoded.getBodies()[0]; - assertEquals(12.0f, decodedBody.getPosition().y, 0.0001f); - BackendBodyHandle restoredBodyHandle = decodedBody.createBackendBody(restore.binding()); - decodedBody.applyToBody(restore.binding(), restoredBodyHandle); - PhysicsBodySnapshot restored = PhysicsBodySnapshots.read(restore.binding(), restoredBodyHandle.value()); - - Assertions.assertNotNull(restored); - assertEquals(ShapeType.PLANE, restored.shapeType()); - assertEquals(12.0f, restored.positionY(), 0.0001f); - } - - @Test - void jointStateCodecRejectsInvalidSpaceIdInsteadOfDefaulting() { - BsonDocument encoded = encodeJoint(persistentJointState(1)); - encoded.put("SpaceId", new BsonInt32(0)); - - assertValidationFails( - () -> PersistentPhysicsJointState.CODEC.decode(encoded, new ExtraInfo()), - "Must be greater than or equal to 1"); - } - - @Test - void jointStateCodecRejectsMissingAxisForAxisJoint() { - BsonDocument encoded = encodeJoint(persistentJointState(1)); - encoded.put("Type", new BsonString("Hinge")); - encoded.remove("Axis"); - - assertValidationFails( - () -> PersistentPhysicsJointState.CODEC.decode(encoded, new ExtraInfo()), - "Persisted HINGE joint requires an axis"); - } - - @Test - void jointStateCodecRejectsInvalidSpringValuesInsteadOfDefaulting() { - BsonDocument encoded = encodeJoint(persistentJointState(1)); - encoded.put("SpringStiffness", new BsonDouble(Double.NaN)); - - assertValidationFails( - () -> PersistentPhysicsJointState.CODEC.decode(encoded, new ExtraInfo()), - "Persisted joint spring stiffness must be finite and >= 0"); - } - - @Test - void stateBlockCodecRejectsOldSchemaVersionBeforeInflating() { - BsonDocument encoded = encodeStateBlock(bodyBlock()); - encoded.put("SchemaVersion", new BsonInt32(4)); - - assertValidationFails( - () -> PersistentPhysicsStateBlock.CODEC.decode(encoded, new ExtraInfo()), - "Must be greater than or equal to " - + PersistentPhysicsWorldResource.CURRENT_SCHEMA_VERSION); - } - - @Test - void stateBlockCodecRejectsUnsupportedEnvelopeFields() { - BsonDocument encoded = encodeStateBlock(bodyBlock()); - encoded.put("Codec", new BsonString("legacy-json-array")); - - assertValidationFails( - () -> PersistentPhysicsStateBlock.CODEC.decode(encoded, new ExtraInfo()), - "Persistent physics state block codec is unsupported"); - } - - @Test - void stateBlockCodecRejectsEmptyPayloadInsteadOfDefaulting() { - BsonDocument encoded = encodeStateBlock(bodyBlock()); - encoded.put("Payload", new BsonBinary(new byte[0])); - - assertValidationFails( - () -> PersistentPhysicsStateBlock.CODEC.decode(encoded, new ExtraInfo()), - "Persistent physics state block payload cannot be empty"); - } - - private static void assertValidationFails(Executable executable, String expectedMessagePart) { - RuntimeException exception = assertThrows(RuntimeException.class, executable); - - assertTrue(exceptionContainsMessage(exception, expectedMessagePart), - () -> "Expected validation message to contain: " + expectedMessagePart - + "\nActual: " + exceptionMessages(exception)); - } - - private static boolean exceptionContainsMessage(Throwable throwable, String expectedMessagePart) { - Throwable current = throwable; - while (current != null) { - if (current.getMessage() != null && current.getMessage().contains(expectedMessagePart)) { - return true; - } - current = current.getCause(); - } - return false; - } - - private static String exceptionMessages(Throwable throwable) { - StringBuilder messages = new StringBuilder(); - Throwable current = throwable; - while (current != null) { - if (!messages.isEmpty()) { - messages.append(" -> "); - } - messages.append(current.getMessage()); - current = current.getCause(); - } - return messages.toString(); - } - - private static BsonDocument encodedSpaceState() { - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - SpaceFixture fixture = spaceFixture("test:space-codec-validation-", settings); - return encodeSpace(PersistentPhysicsSpaceState.from(fixture.binding(), settings)); - } - - private static BsonDocument encodeSpace(PersistentPhysicsSpaceState state) { - return PersistentPhysicsSpaceState.CODEC.encode(state, new ExtraInfo()).asDocument(); - } - - private static BsonDocument encodeBody(PersistentPhysicsBodyState state) { - return PersistentPhysicsBodyState.CODEC.encode(state, new ExtraInfo()).asDocument(); - } - - private static BsonDocument encodeJoint(PersistentPhysicsJointState state) { - return PersistentPhysicsJointState.CODEC.encode(state, new ExtraInfo()).asDocument(); - } - - private static BsonDocument encodeStateBlock(PersistentPhysicsStateBlock block) { - return PersistentPhysicsStateBlock.CODEC.encode(block, new ExtraInfo()).asDocument(); - } - - private static BsonDocument encodeWorld(PersistentPhysicsWorldResource resource) { - return PersistentPhysicsWorldResource.CODEC.encode(resource, new ExtraInfo()).asDocument(); - } - - private static PersistentPhysicsStateBlock bodyBlock() { - return PersistentPhysicsStateBlock.bodyBlocks(new PersistentPhysicsBodyState[] { - persistentBodyState() - })[0]; - } - - private static PersistentPhysicsBodyState persistentBodyState() { - RigidBodyKey bodyId = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000001")); - PhysicsBodyRegistration registration = new PhysicsBodyRegistration( - bodyId, - new BackendBodyHandle(1L), - new SpaceId(1), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - return PersistentPhysicsBodyState.from(registration, boxSnapshot(0.0f)); - } - - private static PersistentPhysicsBodyState persistentPlaneBodyState(float groundY) { - RigidBodyKey bodyId = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000003")); - PhysicsBodyRegistration registration = new PhysicsBodyRegistration( - bodyId, - new BackendBodyHandle(1L), - new SpaceId(1), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - return PersistentPhysicsBodyState.from(registration, planeSnapshot(groundY)); - } - - @Nonnull - private static SpaceFixture spaceFixture(@Nonnull String backendPrefix, - @Nonnull PhysicsSpaceSettings settings) { - FakePhysicsBackend backend = new FakePhysicsBackend(backendPrefix + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), "test-world", settings); - return new SpaceFixture(resource, space); - } - - @Nonnull - private static PhysicsBodySnapshot boxSnapshot(float y) { - return new PhysicsBodySnapshot(new Vector3f(0.0f, y, 0.0f), - new Quaternionf(), - new Vector3f(), - new Vector3f(), - PhysicsBodyType.DYNAMIC, - false, - false, - 0.0f, - ShapeType.BOX, - new Vector3f(0.5f, 0.75f, 1.0f), - 0.0f, - 0.0f, - PhysicsAxis.Y); - } - - @Nonnull - private static PhysicsBodySnapshot planeSnapshot(float groundY) { - return new PhysicsBodySnapshot(new Vector3f(0.0f, groundY, 0.0f), - new Quaternionf(), - new Vector3f(), - new Vector3f(), - PhysicsBodyType.STATIC, - false, - false, - 0.0f, - ShapeType.PLANE, - null, - 0.0f, - 0.0f, - PhysicsAxis.Y); - } - - private record SpaceFixture(@Nonnull LegacyLiveHandleTestResource resource, - @Nonnull PhysicsSpace space) { - - @Nonnull - private PhysicsSpaceBinding binding() { - return resource.requireSpaceBinding(space.id()); - } - } - - private static PersistentPhysicsJointState persistentJointState(int spaceId) { - PersistentPhysicsJointState joint = new PersistentPhysicsJointState(); - joint.setSpaceId(spaceId); - joint.setBodyAKey(RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000001"))); - joint.setBodyBKey(RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000002"))); - joint.setType(PhysicsJointType.FIXED); - return joint; - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsRestorePreflightTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsRestorePreflightTest.java deleted file mode 100644 index 58337533..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/persistence/PersistentPhysicsRestorePreflightTest.java +++ /dev/null @@ -1,184 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.persistence; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import java.lang.reflect.Field; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.Test; - -class PersistentPhysicsRestorePreflightTest { - - private static final AtomicInteger BACKEND_COUNTER = new AtomicInteger(); - - @Test - void acceptsValidPersistedRuntimeSnapshot() { - RuntimeFixture fixture = createRuntimeFixture(); - fixture.syncPersistentState(); - - assertNull(PersistentPhysicsRestorePreflight.validate(fixture.persistent)); - } - - @Test - void rejectsDuplicateSpaceIdsBeforeRuntimeStrip() { - RuntimeFixture fixture = createRuntimeFixture(); - PersistentPhysicsSpaceState first = - fixture.spaceState(); - PersistentPhysicsSpaceState second = first.copy(); - fixture.persistent.setSpaces(new PersistentPhysicsSpaceState[] { first, second }); - - String failure = PersistentPhysicsRestorePreflight.validate(fixture.persistent); - - assertNotNull(failure); - assertTrue(failure.contains("Duplicate persisted space id")); - } - - @Test - void rejectsDuplicateBodyKeysBeforeHydrationCanCreateBackendBodies() { - RuntimeFixture fixture = createRuntimeFixture(); - fixture.syncPersistentState(); - PersistentPhysicsBodyState body = fixture.persistent.getBodies()[0]; - - fixture.persistent.setBodies(new PersistentPhysicsBodyState[] { body, body.copy() }); - String failure = PersistentPhysicsRestorePreflight.validate(fixture.persistent); - - assertNotNull(failure); - assertTrue(failure.contains("Duplicate persisted body key")); - } - - @Test - void rejectsInvalidWorldSettingsBeforeRuntimeStrip() throws Exception { - RuntimeFixture fixture = createRuntimeFixture(); - fixture.syncPersistentState(); - PhysicsWorldSettings invalid = fixture.persistent.getWorldSettings(); - setFloatField(invalid, "maxStepDt", Float.NaN); - fixture.persistent.setWorldSettings(invalid); - - String failure = PersistentPhysicsRestorePreflight.validate(fixture.persistent); - - assertNotNull(failure); - assertTrue(failure.contains("Invalid persisted physics runtime settings")); - } - - @Test - void rejectsInvalidSpaceSettingsBeforeRuntimeStrip() throws Exception { - RuntimeFixture fixture = createRuntimeFixture(); - PersistentPhysicsSpaceState state = - fixture.spaceState(); - setIntField(state, "worldCollisionRadius", 0); - fixture.persistent.setSpaces(new PersistentPhysicsSpaceState[] { state }); - - String failure = PersistentPhysicsRestorePreflight.validate(fixture.persistent); - - assertNotNull(failure); - assertTrue(failure.contains("Invalid persisted physics space settings")); - } - - @Test - void rejectsBlankBackendIdBeforeRuntimeStrip() throws Exception { - RuntimeFixture fixture = createRuntimeFixture(); - PersistentPhysicsSpaceState state = - fixture.spaceState(); - setStringField(state, "backendId", " "); - fixture.persistent.setSpaces(new PersistentPhysicsSpaceState[] { state }); - - String failure = assertDoesNotThrow(() -> PersistentPhysicsRestorePreflight.validate(fixture.persistent)); - - assertNotNull(failure); - assertTrue(failure.contains("Invalid persisted physics space backend id")); - } - - @Test - void rejectsCcdModeForBackendWithoutCcdBeforeRuntimeStrip() { - RuntimeFixture fixture = createRuntimeFixture(); - fixture.syncPersistentState(); - PhysicsWorldSettings settings = fixture.persistent.getWorldSettings(); - settings.setStepMode(PhysicsStepMode.CCD); - fixture.persistent.setWorldSettings(settings); - - String failure = PersistentPhysicsRestorePreflight.validate(fixture.persistent); - - assertNotNull(failure); - assertTrue(failure.contains("CCD mode")); - } - - @Test - void acceptsRuntimeProviderWithoutLegacyBackendRegistration() { - String backendId = "test:persistence-preflight-runtime-" + BACKEND_COUNTER.incrementAndGet(); - Impulse.registerRuntimeProvider(new FakePhysicsBackendRuntimeProvider(backendId)); - PersistentPhysicsWorldResource persistent = new PersistentPhysicsWorldResource(); - PersistentPhysicsSpaceState state = new PersistentPhysicsSpaceState(); - state.setSpaceId(50_001); - state.setBackendId(backendId); - persistent.setWorldSettings(PhysicsWorldSettings.defaults()); - persistent.setSpaces(new PersistentPhysicsSpaceState[] { state }); - - assertNull(PersistentPhysicsRestorePreflight.validate(persistent)); - } - - private static RuntimeFixture createRuntimeFixture() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:persistence-preflight-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource runtime = new LegacyLiveHandleTestResource(); - PhysicsSpace space = runtime.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - runtime.addBody(space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - return new RuntimeFixture(runtime, new PersistentPhysicsWorldResource(), space); - } - - private static void setFloatField(Object target, String fieldName, float value) throws Exception { - Field field = target.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.setFloat(target, value); - } - - private static void setIntField(Object target, String fieldName, int value) throws Exception { - Field field = target.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.setInt(target, value); - } - - private static void setStringField(Object target, String fieldName, String value) throws Exception { - Field field = target.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.set(target, value); - } - - private record RuntimeFixture(LegacyLiveHandleTestResource runtime, - PersistentPhysicsWorldResource persistent, - PhysicsSpace space) { - - private void syncPersistentState() { - persistent.setWorldSettings(runtime.getWorldSettings()); - persistent.setSpaces(new PersistentPhysicsSpaceState[] { - spaceState() - }); - persistent.setBodies(PersistentPhysicsRuntimeSnapshot.capture(runtime).getBodies()); - } - - private PersistentPhysicsSpaceState spaceState() { - return PersistentPhysicsSpaceState.from(runtime.requireSpaceBinding(space.id()), - runtime.getSpaceSettings(space.id())); - } - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsStoreRowCleanupTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsStoreRowCleanupTest.java new file mode 100644 index 00000000..e8d9d720 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsStoreRowCleanupTest.java @@ -0,0 +1,330 @@ +package dev.hytalemodding.impulse.core.internal.physics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.thread.TickingThread; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.FakePhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.physics.PhysicsStoreRowCleanup.BodyEntityRemoval; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendJointHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsSnapshotFrame; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class PhysicsStoreRowCleanupTest { + + @Test + void clearBodyCopiedStateRemovesMultipleBodiesWithoutRemovingRows() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("row-cleanup-batch-copied-state")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(1); + UUID firstBodyUuid = uuid(2); + UUID secondBodyUuid = uuid(3); + UUID retainedBodyUuid = uuid(4); + Ref firstBodyRef = addIdentityRow(store, firstBodyUuid); + Ref secondBodyRef = addIdentityRow(store, secondBodyUuid); + Ref retainedBodyRef = addIdentityRow(store, retainedBodyUuid); + publishCopiedState(store, + spaceUuid, + firstBodyUuid, + firstBodyRef, + secondBodyUuid, + secondBodyRef, + retainedBodyUuid, + retainedBodyRef); + + PhysicsStoreRowCleanup.clearBodyCopiedState(store, + List.of(new BodyEntityRemoval(firstBodyUuid, firstBodyRef), + new BodyEntityRemoval(secondBodyUuid, secondBodyRef))); + + PhysicsSnapshotResource snapshots = + store.getResource(PhysicsSnapshotResource.getResourceType()); + assertNull(snapshots.getBody(firstBodyUuid)); + assertNull(snapshots.getBody(secondBodyUuid)); + assertNotNull(snapshots.getBody(retainedBodyUuid)); + assertNull(PhysicsBodies.spaceId(store, firstBodyUuid)); + assertNull(PhysicsBodies.spaceId(store, secondBodyUuid)); + assertEquals(new SpaceId(42), PhysicsBodies.spaceId(store, retainedBodyUuid)); + assertEquals(1, PhysicsBodies.registrationCount(store)); + assertNotNull(store.getComponent(firstBodyRef, UuidComponent.getComponentType())); + assertNotNull(store.getComponent(secondBodyRef, UuidComponent.getComponentType())); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void refreshIdentityAndRuntimeRefsRebuildsLargeUuidIndexWithoutParallelMapWrites() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("row-cleanup-refresh-large-index")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + List bodyUuids = new ArrayList<>(); + for (int index = 0; index < 6000; index++) { + UUID bodyUuid = uuid(1000L + index); + bodyUuids.add(bodyUuid); + addIdentityRow(store, bodyUuid); + } + + for (int index = 0; index < 20; index++) { + PhysicsStoreRowCleanup.refreshIdentityAndRuntimeRefs(store); + } + + for (UUID bodyUuid : bodyUuids) { + assertNotNull(store.getExternalData().getRefFromUUID(bodyUuid)); + } + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void removeRuntimeBodyRejectsStaleRefThatNoLongerMatchesUuid() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("row-cleanup-stale-body-ref")), + EmptyResourceStorage.get()); + try { + RuntimeFixture fixture = addBoundSpace(store, + uuid(30), + new BackendId("test:row-cleanup-body")); + UUID firstBodyUuid = uuid(31); + UUID secondBodyUuid = uuid(32); + Ref firstBodyRef = addIdentityRow(store, firstBodyUuid); + Ref secondBodyRef = addIdentityRow(store, secondBodyUuid); + BackendBodyHandle firstHandle = new BackendBodyHandle(301L); + BackendBodyHandle secondHandle = new BackendBodyHandle(302L); + PhysicsRuntimeResource runtime = + store.getResource(PhysicsRuntimeResource.getResourceType()); + bindBody(runtime, fixture, firstBodyUuid, firstBodyRef, firstHandle); + bindBody(runtime, fixture, secondBodyUuid, secondBodyRef, secondHandle); + + boolean removed = PhysicsStoreRowCleanup.removeRuntimeBody(store, + runtime, + firstBodyUuid, + secondBodyRef); + + assertFalse(removed); + assertEquals(firstHandle, runtime.getBodyHandle(firstBodyRef)); + assertEquals(secondHandle, runtime.getBodyHandle(secondBodyRef)); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void removeRuntimeJointRejectsStaleRefThatNoLongerMatchesUuid() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("row-cleanup-stale-joint-ref")), + EmptyResourceStorage.get()); + try { + RuntimeFixture fixture = addBoundSpace(store, + uuid(40), + new BackendId("test:row-cleanup-joint")); + UUID firstJointUuid = uuid(41); + UUID secondJointUuid = uuid(42); + Ref firstJointRef = addIdentityRow(store, firstJointUuid); + Ref secondJointRef = addIdentityRow(store, secondJointUuid); + BackendJointHandle firstHandle = new BackendJointHandle(401L); + BackendJointHandle secondHandle = new BackendJointHandle(402L); + PhysicsRuntimeResource runtime = + store.getResource(PhysicsRuntimeResource.getResourceType()); + bindJoint(runtime, fixture, firstJointUuid, firstJointRef, firstHandle); + bindJoint(runtime, fixture, secondJointUuid, secondJointRef, secondHandle); + + boolean removed = PhysicsStoreRowCleanup.removeRuntimeJoint(store, + runtime, + firstJointUuid, + secondJointRef); + + assertFalse(removed); + assertEquals(firstHandle, runtime.getJointHandle(firstJointRef)); + assertEquals(secondHandle, runtime.getJointHandle(secondJointRef)); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Nonnull + private static Ref addIdentityRow(@Nonnull Store store, + @Nonnull UUID bodyUuid) { + Ref ref = store.addEntity(PhysicsEntities.entityHolder(store, bodyUuid), + AddReason.SPAWN); + assertNotNull(ref); + return ref; + } + + @Nonnull + private static RuntimeFixture addBoundSpace(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull BackendId backendId) { + Ref spaceRef = store.addEntity(PhysicsEntities.spaceHolder(store, + spaceUuid, + new SpaceComponent(backendId, new Vector3f(0.0f, -9.81f, 0.0f))), + AddReason.SPAWN); + assertNotNull(spaceRef); + FakePhysicsBackendRuntime backendRuntime = (FakePhysicsBackendRuntime) + new FakePhysicsBackendRuntimeProvider(backendId, false, false).createRuntime(); + BackendSpaceHandle spaceHandle = + new BackendSpaceHandle(backendRuntime.createSpace(new SpaceId(42))); + PhysicsRuntimeResource runtime = + store.getResource(PhysicsRuntimeResource.getResourceType()); + runtime.putRuntime(backendId, backendRuntime); + runtime.putSpaceHandle(spaceRef, backendId, spaceHandle); + runtime.putSpaceMetadata(backendId, spaceHandle, spaceUuid, spaceRef); + return new RuntimeFixture(spaceUuid, backendId, spaceRef, spaceHandle); + } + + private static void bindBody(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull RuntimeFixture fixture, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef, + @Nonnull BackendBodyHandle bodyHandle) { + runtime.putBodyHandle(bodyRef, fixture.spaceRef(), fixture.spaceHandle(), bodyHandle); + runtime.putBodySnapshotMetadata(fixture.backendId(), + fixture.spaceHandle(), + bodyHandle, + bodyUuid, + bodyRef, + fixture.spaceUuid()); + } + + private static void bindJoint(@Nonnull PhysicsRuntimeResource runtime, + @Nonnull RuntimeFixture fixture, + @Nonnull UUID jointUuid, + @Nonnull Ref jointRef, + @Nonnull BackendJointHandle jointHandle) { + runtime.putJointHandle(jointRef, fixture.spaceRef(), fixture.spaceHandle(), jointHandle); + runtime.putJointMetadata(fixture.backendId(), + fixture.spaceHandle(), + jointHandle, + jointUuid, + jointRef); + } + + private static void publishCopiedState(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull UUID firstBodyUuid, + @Nonnull Ref firstBodyRef, + @Nonnull UUID secondBodyUuid, + @Nonnull Ref secondBodyRef, + @Nonnull UUID retainedBodyUuid, + @Nonnull Ref retainedBodyRef) { + store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .putSpace(new SpaceId(42), spaceUuid); + store.getResource(PhysicsSnapshotResource.getResourceType()) + .publish(new PhysicsSnapshotFrame(1L, + 0.05f, + List.of(snapshot(firstBodyRef, firstBodyUuid, spaceUuid), + snapshot(secondBodyRef, secondBodyUuid, spaceUuid), + snapshot(retainedBodyRef, retainedBodyUuid, spaceUuid)))); + } + + @Nonnull + private static PhysicsBodySnapshot snapshot(@Nonnull Ref bodyRef, + @Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid) { + return PhysicsBodySnapshot.of(bodyRef, + bodyUuid, + spaceUuid, + PhysicsBodyType.DYNAMIC, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + false); + } + + @Nonnull + private static UUID uuid(long leastSignificantBits) { + return new UUID(0L, leastSignificantBits); + } + + private static void markCurrentThreadAsWorldThread(@Nonnull Store store) { + try { + Method setThread = TickingThread.class.getDeclaredMethod("setThread", Thread.class); + setThread.setAccessible(true); + setThread.invoke(store.getExternalData().getWorld(), Thread.currentThread()); + } catch (NoSuchMethodException | IllegalAccessException exception) { + throw new AssertionError("Could not mark test world thread", exception); + } catch (InvocationTargetException exception) { + throw new AssertionError("Could not mark test world thread", + exception.getTargetException()); + } + } + + private record RuntimeFixture(@Nonnull UUID spaceUuid, + @Nonnull BackendId backendId, + @Nonnull Ref spaceRef, + @Nonnull BackendSpaceHandle spaceHandle) { + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsStoreTopologyMutationsTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsStoreTopologyMutationsTest.java new file mode 100644 index 00000000..564ed67f --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/physics/PhysicsStoreTopologyMutationsTest.java @@ -0,0 +1,467 @@ +package dev.hytalemodding.impulse.core.internal.physics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.thread.TickingThread; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.PhysicsAxis; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendJointType; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.FakePhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkStoreTypes; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendJointHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsJointEntities; +import dev.hytalemodding.impulse.core.plugin.components.JointType; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsSnapshotFrame; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class PhysicsStoreTopologyMutationsTest { + + @Test + void addSpaceIndexesRefInPhysicsStoreUuidMap() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("topology-add-space-uuid-map-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(100); + Ref spaceRef = PhysicsSpaceMutations.addSpace(store, + spaceUuid, + new SpaceId(100), + new BackendId("test:topology-add-space-uuid-map")); + + assertSame(spaceRef, store.getExternalData().getRefFromUUID(spaceUuid)); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void destroyBodyRemovesDependentJointRowAndRuntimeHandles() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("topology-destroy-body-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(1); + UUID bodyAUuid = uuid(2); + UUID bodyBUuid = uuid(3); + UUID jointUuid = uuid(4); + BackendId backendId = new BackendId("test:topology-cleanup"); + BoundSpace space = addBoundSpace(store, spaceUuid, backendId); + Ref bodyARef = addBody(store, spaceUuid, space.ref(), bodyAUuid); + Ref bodyBRef = addBody(store, spaceUuid, space.ref(), bodyBUuid); + Ref jointRef = addJoint(store, + spaceUuid, + space.ref(), + bodyAUuid, + bodyARef, + bodyBUuid, + bodyBRef, + jointUuid); + BackendBodyHandle bodyAHandle = bindBody(store, + space, + bodyAUuid, + bodyARef, + 0.0f); + bindBody(store, space, bodyBUuid, bodyBRef, 2.0f); + bindJoint(store, space, jointUuid, jointRef, bodyAHandle, bodyBRef); + publishCopiedState(store, spaceUuid, bodyAUuid, bodyARef, bodyBUuid, bodyBRef); + + PhysicsTopologyMutations.destroyBody(store, bodyAUuid); + + PhysicsRuntimeResource runtime = store.getResource( + PhysicsRuntimeResource.getResourceType()); + PhysicsSnapshotResource snapshots = + store.getResource(PhysicsSnapshotResource.getResourceType()); + assertNull(store.getExternalData().getRefFromUUID(bodyAUuid)); + assertNull(store.getExternalData().getRefFromUUID(jointUuid)); + Ref remainingBodyRef = + store.getExternalData().getRefFromUUID(bodyBUuid); + assertNotNull(remainingBodyRef); + assertNull(runtime.getBodyHandle(bodyARef)); + assertNull(runtime.getJointHandle(jointRef)); + assertNotNull(runtime.getBodyHandle(remainingBodyRef)); + assertEquals(1, space.runtime().bodyCount(space.handle().value())); + assertEquals(0, space.runtime().jointCount(space.handle().value())); + assertNull(snapshots.getBody(bodyAUuid)); + assertNotNull(snapshots.getBody(bodyBUuid)); + assertFalse(PhysicsBodies.isRegistered(store, bodyAUuid)); + assertEquals(new SpaceId(42), PhysicsBodies.spaceId(store, bodyBUuid)); + assertFalse(bodyARef.isValid()); + assertFalse(jointRef.isValid()); + assertNotNull(store.getComponent(remainingBodyRef, BodyComponent.getComponentType())); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void clearBodiesKeepingSpacesRecoversFailedRestoreStatus() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + PhysicsChunkStoreTypes.registerPhysicsStoreResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("topology-clean-recovers-restore")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(1); + UUID bodyUuid = uuid(2); + BoundSpace space = addBoundSpace(store, + spaceUuid, + new BackendId("test:topology-clean-recovers-restore")); + Ref bodyRef = addBody(store, spaceUuid, space.ref(), bodyUuid); + bindBody(store, space, bodyUuid, bodyRef, 0.0f); + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + restore.markFailed("backend binding failed"); + + PhysicsTopologyMutations.clearBodiesKeepingSpaces(store); + + assertFalse(restore.isFailed(), restore.getFailureMessage()); + assertFalse(restore.isPending()); + assertTrue(restore.isHydrated()); + } finally { + registry.removeStore(store); + registry.shutdown(); + PhysicsChunkStoreTypes.clearPhysicsStoreResourceTypes(); + } + } + + @Test + void clearBodiesKeepingSpacesClosesUntrackedRuntimeSpacesAfterFailedRestore() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + PhysicsChunkStoreTypes.registerPhysicsStoreResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("topology-clean-closes-orphan-runtime")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(1); + BackendId backendId = new BackendId("test:topology-clean-orphan-runtime"); + Ref spaceRef = store.addEntity(PhysicsEntities.spaceHolder(store, + spaceUuid, + new SpaceComponent(backendId, new Vector3f(0.0f, -9.81f, 0.0f))), + AddReason.SPAWN); + assertNotNull(spaceRef); + store.getExternalData().putRefForUUID(spaceUuid, spaceRef); + SpaceId compatibilitySpaceId = new SpaceId(42); + store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .putSpace(compatibilitySpaceId, spaceUuid); + FakePhysicsBackendRuntime staleRuntime = (FakePhysicsBackendRuntime) + new FakePhysicsBackendRuntimeProvider(backendId, false, false).createRuntime(); + staleRuntime.createSpace(compatibilitySpaceId); + PhysicsRuntimeResource runtime = store.getResource( + PhysicsRuntimeResource.getResourceType()); + runtime.putRuntime(backendId, staleRuntime); + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + restore.markFailed("PhysicsStore space " + spaceUuid + + " failed backend binding: Space already exists: " + compatibilitySpaceId); + + PhysicsTopologyMutations.clearBodiesKeepingSpaces(store); + + assertFalse(staleRuntime.hasSpace(compatibilitySpaceId.value())); + assertNull(runtime.getRuntime(backendId)); + assertFalse(restore.isFailed(), restore.getFailureMessage()); + assertTrue(restore.isHydrated()); + assertEquals(spaceUuid, + store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .getSpaceUuid(compatibilitySpaceId)); + assertNotNull(store.getComponent(spaceRef, SpaceComponent.getComponentType())); + } finally { + registry.removeStore(store); + registry.shutdown(); + PhysicsChunkStoreTypes.clearPhysicsStoreResourceTypes(); + } + } + + @Nonnull + private static BoundSpace addBoundSpace(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull BackendId backendId) { + Ref spaceRef = store.addEntity(PhysicsEntities.spaceHolder(store, + spaceUuid, + new SpaceComponent(backendId, new Vector3f(0.0f, -9.81f, 0.0f))), + AddReason.SPAWN); + assertNotNull(spaceRef); + store.getExternalData().putRefForUUID(spaceUuid, spaceRef); + PhysicsBackendRuntime backendRuntime = + new FakePhysicsBackendRuntimeProvider(backendId, false, false).createRuntime(); + BackendSpaceHandle spaceHandle = + new BackendSpaceHandle(backendRuntime.createSpace(new SpaceId(42))); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + runtime.putRuntime(backendId, backendRuntime); + runtime.putSpaceHandle(spaceRef, backendId, spaceHandle); + runtime.putSpaceMetadata(backendId, spaceHandle, spaceUuid, spaceRef); + store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .putSpace(new SpaceId(42), spaceUuid); + return new BoundSpace(spaceUuid, spaceRef, backendRuntime, spaceHandle, backendId); + } + + @Nonnull + private static Ref addBody(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull Ref spaceRef, + @Nonnull UUID bodyUuid) { + BodyComponent body = new BodyComponent(spaceUuid); + body.setSpaceRef(spaceRef); + Ref bodyRef = store.addEntity(PhysicsEntities.bodyHolder(store, + bodyUuid, + body, + new DynamicsComponent(PhysicsBodyType.DYNAMIC, 1.0f, 0.0f, 0.0f, false), + null, + new ColliderComponent(new Vector3f(), new Quaternionf(), false), + new ShapeComponent(ShapeType.BOX, + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + PhysicsAxis.Y, + 0.0f, + ""), + new MaterialComponent(0.5f, 0.1f), + new CollisionFilterComponent(0x01, 0x02)), + AddReason.SPAWN); + assertNotNull(bodyRef); + store.getExternalData().putRefForUUID(bodyUuid, bodyRef); + return bodyRef; + } + + @Nonnull + private static Ref addJoint(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull Ref spaceRef, + @Nonnull UUID bodyAUuid, + @Nonnull Ref bodyARef, + @Nonnull UUID bodyBUuid, + @Nonnull Ref bodyBRef, + @Nonnull UUID jointUuid) { + JointComponent joint = PhysicsJointEntities.joint(spaceRef, + bodyARef, + bodyBRef, + JointType.FIXED, + new Vector3f(), + new Vector3f(), + new Vector3f(0.0f, 1.0f, 0.0f)); + joint.setSpaceUuid(spaceUuid); + joint.setBodyAUuid(bodyAUuid); + joint.setBodyBUuid(bodyBUuid); + Ref jointRef = store.addEntity(PhysicsEntities.jointHolder(store, + jointUuid, + joint), + AddReason.SPAWN); + assertNotNull(jointRef); + store.getExternalData().putRefForUUID(jointUuid, jointRef); + return jointRef; + } + + @Nonnull + private static BackendBodyHandle bindBody(@Nonnull Store store, + @Nonnull BoundSpace space, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef, + float positionX) { + long bodyId = space.runtime().createBody(space.handle().value(), + BackendRuntimeCodes.shapeTypeCode(ShapeType.BOX), + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + BackendRuntimeCodes.axisCode(PhysicsAxis.Y), + 0.0f, + 1.0f, + BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC), + positionX, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + BackendBodyHandle handle = new BackendBodyHandle(bodyId); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + runtime.putBodyHandle(bodyRef, space.ref(), space.handle(), handle); + runtime.putBodySnapshotMetadata(space.backendId(), + space.handle(), + handle, + bodyUuid, + bodyRef, + space.uuid()); + runtime.putBodyHitMetadata(space.backendId(), + space.handle(), + handle, + bodyUuid, + bodyRef, + PhysicsBodyType.DYNAMIC, + ShapeType.BOX); + return handle; + } + + private static void bindJoint(@Nonnull Store store, + @Nonnull BoundSpace space, + @Nonnull UUID jointUuid, + @Nonnull Ref jointRef, + @Nonnull BackendBodyHandle bodyAHandle, + @Nonnull Ref bodyBRef) { + BackendBodyHandle bodyBHandle = store.getResource(PhysicsRuntimeResource.getResourceType()) + .getBodyHandle(bodyBRef); + assertNotNull(bodyBHandle); + long jointId = space.runtime().createJoint(space.handle().value(), + BackendRuntimeCodes.jointTypeCode(BackendJointType.FIXED), + bodyAHandle.value(), + bodyBHandle.value(), + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + false, + 0.0f, + 0.0f); + BackendJointHandle handle = new BackendJointHandle(jointId); + store.getResource(PhysicsRuntimeResource.getResourceType()) + .putJointHandle(jointRef, space.ref(), space.handle(), handle); + store.getResource(PhysicsRuntimeResource.getResourceType()) + .putJointMetadata(space.backendId(), space.handle(), handle, jointUuid, jointRef); + } + + private static void publishCopiedState(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull UUID bodyAUuid, + @Nonnull Ref bodyARef, + @Nonnull UUID bodyBUuid, + @Nonnull Ref bodyBRef) { + store.getResource(PhysicsSnapshotResource.getResourceType()) + .publish(new PhysicsSnapshotFrame(1L, + 0.05f, + List.of(snapshot(bodyARef, bodyAUuid, spaceUuid), + snapshot(bodyBRef, bodyBUuid, spaceUuid)))); + } + + @Nonnull + private static PhysicsBodySnapshot snapshot(@Nonnull Ref bodyRef, + @Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid) { + return PhysicsBodySnapshot.of(bodyRef, + bodyUuid, + spaceUuid, + PhysicsBodyType.DYNAMIC, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + false); + } + + private static void markCurrentThreadAsWorldThread(@Nonnull Store store) { + try { + Method setThread = TickingThread.class.getDeclaredMethod("setThread", Thread.class); + setThread.setAccessible(true); + setThread.invoke(store.getExternalData().getWorld(), Thread.currentThread()); + } catch (NoSuchMethodException | IllegalAccessException exception) { + throw new AssertionError("Could not mark test world thread", exception); + } catch (InvocationTargetException exception) { + throw new AssertionError("Could not mark test world thread", + exception.getTargetException()); + } + } + + @Nonnull + private static UUID uuid(long leastSignificantBits) { + return new UUID(0L, leastSignificantBits); + } + + private record BoundSpace(@Nonnull UUID uuid, + @Nonnull Ref ref, + @Nonnull PhysicsBackendRuntime runtime, + @Nonnull BackendSpaceHandle handle, + @Nonnull BackendId backendId) { + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/physics/persistence/PhysicsStoreHolderPersistenceTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/physics/persistence/PhysicsStoreHolderPersistenceTest.java new file mode 100644 index 00000000..31041af6 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/physics/persistence/PhysicsStoreHolderPersistenceTest.java @@ -0,0 +1,610 @@ +package dev.hytalemodding.impulse.core.internal.persistence; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.BsonUtil; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.ImpulseBackendRegistry; +import dev.hytalemodding.impulse.api.PhysicsAxis; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.FakePhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkStoreTypes; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionRestoreDependencyComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent.PartKind; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.PhysicsStoreRegistration; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStepSchedulerResource; +import dev.hytalemodding.impulse.core.internal.systems.PersistenceHydrationSystem; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.early.PhysicsStoreHooks; +import dev.hytalemodding.impulse.core.plugin.components.BodyCommandComponent; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.ChunkCollisionSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsSnapshotFrame; +import java.lang.reflect.Field; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; +import org.bson.BsonArray; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonInt32; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class PhysicsStoreHolderPersistenceTest { + + private static final UUID SPACE_UUID = uuid(1); + private static final UUID BODY_A_UUID = uuid(2); + private static final UUID BODY_B_UUID = uuid(3); + private static final UUID GENERATED_BODY_UUID = uuid(4); + private static final UUID JOINT_UUID = uuid(5); + private static final BackendId HOLDER_BACKEND_ID = new BackendId("test:holder-persistence"); + + @TempDir + Path tempDir; + + @Test + void holderBlobsPersistUuidBodyRowsWithSnapshotTargetsOnly() { + StoreFixture fixture = store("holder-capture", tempDir.resolve("capture")); + try { + Ref spaceRef = addSpace(fixture.store(), SPACE_UUID); + Ref bodyARef = addBody(fixture.store(), + BODY_A_UUID, + spaceRef, + null); + Ref bodyBRef = addBody(fixture.store(), + BODY_B_UUID, + spaceRef, + null); + fixture.store().putComponent(bodyARef, + BodyCommandComponent.getComponentType(), + BodyCommandComponent.wake()); + addBody(fixture.store(), + GENERATED_BODY_UUID, + spaceRef, + new ChunkCollisionSourceComponent("0:0:0", + 0, + 0, + 0, + "chunk-collision/0/0/0", + PartKind.BOX, + 0)); + addJoint(fixture.store(), JOINT_UUID, BODY_A_UUID, BODY_B_UUID); + publishSnapshot(fixture.store(), bodyARef); + + List> decoded = PhysicsStoreHolderPersistence + .capturePersistentHolderBlobs(fixture.store()) + .stream() + .map(blob -> PhysicsStoreHolderPersistence.decodeHolder( + fixture.store().getRegistry(), + blob)) + .toList(); + + assertNotNull(holder(decoded, SPACE_UUID)); + Holder bodyA = holder(decoded, BODY_A_UUID); + assertNotNull(bodyA); + assertNotNull(holder(decoded, BODY_B_UUID)); + assertNotNull(holder(decoded, JOINT_UUID)); + assertNull(holder(decoded, GENERATED_BODY_UUID)); + + assertNull(bodyA.getComponent(BodyCommandComponent.getComponentType())); + BodyComponent body = bodyA.getComponent(BodyComponent.getComponentType()); + assertNotNull(body); + assertNull(body.getSpaceRef()); + TargetComponent target = bodyA.getComponent(TargetComponent.getComponentType()); + assertNotNull(target); + assertFalse(target.isActive()); + assertFalse(target.isActivate()); + assertEquals(new Vector3f(9.0f, 8.0f, 7.0f), target.getPosition()); + assertEquals(new Vector3f(0.1f, 0.2f, 0.3f), target.getLinearVelocity()); + assertEquals(new Vector3f(0.4f, 0.5f, 0.6f), target.getAngularVelocity()); + } finally { + fixture.close(); + } + } + + @Test + void holderCaptureAddsChunkCollisionRestoreDependencyToDynamicBodiesNearGeneratedRows() { + StoreFixture fixture = store("holder-capture-restore-context", + tempDir.resolve("capture-restore-context")); + try { + Ref spaceRef = addSpace(fixture.store(), SPACE_UUID); + fixture.store().putComponent(spaceRef, + ChunkCollisionSettingsComponent.getComponentType(), + new ChunkCollisionSettingsComponent(PhysicsChunkCollisionMode.STREAMING, + false, + 8, + 4, + 100)); + addBody(fixture.store(), BODY_A_UUID, spaceRef, null); + addBody(fixture.store(), + GENERATED_BODY_UUID, + spaceRef, + new ChunkCollisionSourceComponent("chunk:0:0:0", + 0, + 0, + 0, + "chunk-collision/0/0/0", + PartKind.BOX, + 0)); + + List> decoded = PhysicsStoreHolderPersistence + .capturePersistentHolderBlobs(fixture.store()) + .stream() + .map(blob -> PhysicsStoreHolderPersistence.decodeHolder( + fixture.store().getRegistry(), + blob)) + .toList(); + + assertNull(holder(decoded, GENERATED_BODY_UUID)); + ChunkCollisionRestoreDependencyComponent dependency = holder(decoded, BODY_A_UUID) + .getComponent(ChunkCollisionRestoreDependencyComponent.getComponentType()); + assertNotNull(dependency); + assertEquals(SPACE_UUID, dependency.getSpaceUuid()); + assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), dependency.getCenter()); + assertEquals(4, dependency.getRadius()); + assertEquals(PhysicsChunkCollisionMode.STREAMING, dependency.getModeAtSave()); + } finally { + fixture.close(); + } + } + + @Test + void holderStorageUsesWorldOwnedPhysicsStoreDirectory() { + Path savePath = tempDir.resolve("storage-layout"); + StoreFixture fixture = store("storage-layout", savePath); + try { + Path file = PhysicsStoreHolderStorage.file(fixture.store().getExternalData()); + + assertEquals(savePath.resolve("physicsstore").resolve("holders.bson"), + file); + } finally { + fixture.close(); + } + } + + @Test + void holderSaveCreatesWorldOwnedPhysicsStoreDirectory() { + Path savePath = tempDir.resolve("chunk-shaped-layout"); + StoreFixture fixture = store("chunk-shaped-layout", savePath); + try { + Ref spaceRef = addSpace(fixture.store(), SPACE_UUID); + addBody(fixture.store(), + BODY_A_UUID, + spaceRef, + null); + + PhysicsStoreHolderStorage.save(fixture.store()).join(); + + assertTrue(Files.exists(savePath.resolve("physicsstore") + .resolve("holders.bson"))); + assertTrue(Files.exists(savePath.resolve("physicsstore"))); + assertFalse(Files.exists(savePath.resolve("resources") + .resolve("PhysicsStoreHolders.bson"))); + } finally { + fixture.close(); + } + } + + @Test + void holderHydrationRejectsDuplicateUuidWithoutAddingPartialRows() { + StoreFixture fixture = store("holder-duplicate-uuid", + tempDir.resolve("duplicate-uuid")); + try { + Holder first = PhysicsEntities.spaceHolder(fixture.store(), + SPACE_UUID, + new SpaceComponent(new BackendId("test:holder-persistence"), + new Vector3f(0.0f, -9.81f, 0.0f))); + Holder second = fixture.store().getRegistry().newHolder(); + second.addComponent(UuidComponent.getComponentType(), new UuidComponent(SPACE_UUID)); + second.addComponent(BodyComponent.getComponentType(), + new BodyComponent(SPACE_UUID)); + writeHolderStorage(fixture.store(), List.of(first, second)); + + new PersistenceHydrationSystem().tick(0.0f, 0, fixture.store()); + + PhysicsRestoreStatusResource restore = fixture.store().getResource( + PhysicsRestoreStatusResource.getResourceType()); + assertTrue(restore.isFailed()); + assertFalse(restore.isHydrated()); + assertTrue(rowUuids(fixture.store()).isEmpty()); + } finally { + fixture.close(); + } + } + + @Test + void holderHydrationRejectsBodyWithoutSavedSpaceWithoutAddingPartialRows() { + StoreFixture fixture = store("holder-missing-space", + tempDir.resolve("missing-space")); + try { + Holder body = fixture.store().getRegistry().newHolder(); + body.addComponent(UuidComponent.getComponentType(), new UuidComponent(BODY_A_UUID)); + body.addComponent(BodyComponent.getComponentType(), + new BodyComponent(SPACE_UUID)); + writeHolderStorage(fixture.store(), List.of(body)); + + new PersistenceHydrationSystem().tick(0.0f, 0, fixture.store()); + + PhysicsRestoreStatusResource restore = fixture.store().getResource( + PhysicsRestoreStatusResource.getResourceType()); + assertTrue(restore.isFailed()); + assertFalse(restore.isHydrated()); + assertTrue(rowUuids(fixture.store()).isEmpty()); + } finally { + fixture.close(); + } + } + + @Test + void registeredStoreReloadBindsSavedBodiesOnce() { + FakePhysicsBackendRuntimeProvider provider = + new FakePhysicsBackendRuntimeProvider(HOLDER_BACKEND_ID, false, false); + ImpulseBackendRegistry.registerRuntimeProvider(provider); + Path savePath = tempDir.resolve("registered-reload"); + + StoreFixture source = registeredStore("registered-reload-source", savePath); + try { + Ref spaceRef = addSpace(source.store(), SPACE_UUID); + addBody(source.store(), BODY_A_UUID, spaceRef, null); + addBody(source.store(), BODY_B_UUID, spaceRef, null); + + source.store().tick(0.0f); + + BackendSpaceHandle sourceHandle = source.store() + .getResource(PhysicsRuntimeResource.getResourceType()) + .getSpaceHandle(spaceRef); + assertNotNull(sourceHandle); + assertEquals(1, provider.createdRuntimes().size()); + assertEquals(2, provider.createdRuntimes().get(0).bodyCount(sourceHandle.value())); + + PhysicsStoreHooks.shutdown(source.store().getExternalData()); + } finally { + source.close(); + } + + StoreFixture target = registeredStore("registered-reload-target", savePath); + try { + target.store().tick(0.0f); + + PhysicsRestoreStatusResource restore = target.store().getResource( + PhysicsRestoreStatusResource.getResourceType()); + assertTrue(restore.isHydrated()); + assertFalse(restore.isFailed()); + assertTrue(restore.getSoftSkipsByReason().isEmpty()); + List rowUuids = rowUuids(target.store()); + assertEquals(3, rowUuids.size()); + assertTrue(rowUuids.contains(SPACE_UUID)); + assertTrue(rowUuids.contains(BODY_A_UUID)); + assertTrue(rowUuids.contains(BODY_B_UUID)); + + Ref spaceRef = target.store().getExternalData().getRefFromUUID( + SPACE_UUID); + assertNotNull(spaceRef); + PhysicsRuntimeResource runtime = target.store().getResource( + PhysicsRuntimeResource.getResourceType()); + BackendSpaceHandle handle = runtime.getSpaceHandle(spaceRef); + assertNotNull(handle); + assertEquals(2, provider.createdRuntimes().size()); + FakePhysicsBackendRuntime reloadedRuntime = provider.createdRuntimes().get(1); + assertTrue(reloadedRuntime.hasSpace(handle.value())); + assertEquals(2, reloadedRuntime.bodyCount(handle.value())); + + target.store().tick(0.05f); + target.store() + .getResource(PhysicsStepSchedulerResource.getResourceType()) + .whenIdle() + .toCompletableFuture() + .join(); + target.store().tick(0.0f); + + assertEquals(2, target.store() + .getResource(PhysicsSnapshotResource.getResourceType()) + .getLatestFrame() + .bodies() + .size()); + } finally { + target.close(); + } + } + + @Test + void holderHydrationClosesStaleUntrackedBackendRuntimeBeforeRebinding() { + FakePhysicsBackendRuntimeProvider provider = + new FakePhysicsBackendRuntimeProvider(HOLDER_BACKEND_ID, false, false); + ImpulseBackendRegistry.registerRuntimeProvider(provider); + Path savePath = tempDir.resolve("stale-backend-runtime"); + SpaceId compatibilitySpaceId = new SpaceId(2); + + StoreFixture source = store("stale-runtime-source", savePath); + try { + Ref spaceRef = addSpace(source.store(), SPACE_UUID); + addBody(source.store(), BODY_A_UUID, spaceRef, null); + PhysicsStoreHolderStorage.save(source.store()).join(); + } finally { + source.close(); + } + + StoreFixture target = registeredStore("stale-runtime-target", savePath); + try { + FakePhysicsBackendRuntime staleRuntime = (FakePhysicsBackendRuntime) + provider.createRuntime(); + staleRuntime.createSpace(compatibilitySpaceId); + target.store() + .getResource(PhysicsRuntimeResource.getResourceType()) + .putRuntime(HOLDER_BACKEND_ID, staleRuntime); + target.store() + .getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .putSpace(compatibilitySpaceId, SPACE_UUID); + + target.store().tick(0.0f); + + PhysicsRestoreStatusResource restore = target.store().getResource( + PhysicsRestoreStatusResource.getResourceType()); + assertTrue(restore.isHydrated()); + assertFalse(restore.isFailed(), restore.getFailureMessage()); + assertFalse(staleRuntime.hasSpace(compatibilitySpaceId.value())); + assertEquals(2, provider.createdRuntimes().size()); + FakePhysicsBackendRuntime reboundRuntime = provider.createdRuntimes().get(1); + assertTrue(reboundRuntime.hasSpace(compatibilitySpaceId.value())); + assertEquals(1, reboundRuntime.bodyCount(compatibilitySpaceId.value())); + } finally { + target.close(); + } + } + + @Nonnull + private static StoreFixture store(@Nonnull String worldName, @Nonnull Path savePath) { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + PhysicsStore physicsStore = new PhysicsStore(world(worldName, savePath)); + Store store = registry.addStore(physicsStore, EmptyResourceStorage.get()); + setField(physicsStore, PhysicsStore.class, "store", store); + return new StoreFixture(registry, store); + } + + @Nonnull + private static StoreFixture registeredStore(@Nonnull String worldName, @Nonnull Path savePath) { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsChunkStoreTypes.registerPhysicsStoreResourceTypes(proxy); + PhysicsStoreRegistration.register(proxy); + PhysicsStore physicsStore = new PhysicsStore(world(worldName, savePath)); + Store store = registry.addStore(physicsStore, EmptyResourceStorage.get()); + setField(physicsStore, PhysicsStore.class, "store", store); + return new StoreFixture(registry, store); + } + + @Nonnull + private static World world(@Nonnull String worldName, @Nonnull Path savePath) { + World world = TestInstanceFactory.world(worldName); + setField(world, World.class, "savePath", savePath); + return world; + } + + @Nonnull + private static Ref addSpace(@Nonnull Store store, + @Nonnull UUID spaceUuid) { + Ref ref = store.addEntity(PhysicsEntities.spaceHolder(store, + spaceUuid, + new SpaceComponent(HOLDER_BACKEND_ID, new Vector3f(0.0f, -9.81f, 0.0f))), + AddReason.SPAWN); + assertNotNull(ref); + return ref; + } + + @Nonnull + private static Ref addBody(@Nonnull Store store, + @Nonnull UUID bodyUuid, + @Nonnull Ref spaceRef, + ChunkCollisionSourceComponent source) { + Holder holder = PhysicsEntities.bodyHolder(store, + bodyUuid, + body(SPACE_UUID, spaceRef), + new DynamicsComponent(PhysicsBodyType.DYNAMIC, 1.0f, 0.0f, 0.0f, false), + target(), + new ColliderComponent(new Vector3f(), new Quaternionf(), false), + new ShapeComponent(ShapeType.BOX, + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + PhysicsAxis.Y, + 0.0f, + ""), + new MaterialComponent(0.6f, 0.1f), + new CollisionFilterComponent(PhysicsCollisionFilters.DYNAMIC_BODY, + PhysicsCollisionFilters.ALL)); + if (source != null) { + holder.addComponent(ChunkCollisionSourceComponent.getComponentType(), source); + } + Ref ref = store.addEntity(holder, AddReason.SPAWN); + assertNotNull(ref); + return ref; + } + + @Nonnull + private static BodyComponent body(@Nonnull UUID spaceUuid, + @Nonnull Ref spaceRef) { + BodyComponent body = new BodyComponent(spaceUuid); + body.setSpaceRef(spaceRef); + return body; + } + + @Nonnull + private static TargetComponent target() { + TargetComponent target = new TargetComponent(); + target.setActive(true); + target.setPosition(new Vector3f(1.0f, 2.0f, 3.0f)); + return target; + } + + private static void addJoint(@Nonnull Store store, + @Nonnull UUID jointUuid, + @Nonnull UUID bodyAUuid, + @Nonnull UUID bodyBUuid) { + Holder holder = store.getRegistry().newHolder(); + holder.addComponent(UuidComponent.getComponentType(), new UuidComponent(jointUuid)); + JointComponent joint = new JointComponent(); + joint.setSpaceUuid(SPACE_UUID); + joint.setBodyAUuid(bodyAUuid); + joint.setBodyBUuid(bodyBUuid); + holder.addComponent(JointComponent.getComponentType(), joint); + assertNotNull(store.addEntity(holder, AddReason.SPAWN)); + } + + private static void publishSnapshot(@Nonnull Store store, + @Nonnull Ref bodyRef) { + store.getResource(PhysicsSnapshotResource.getResourceType()).publish( + new PhysicsSnapshotFrame(1L, + 0.05f, + List.of(new PhysicsBodySnapshot(bodyRef, + BODY_A_UUID, + SPACE_UUID, + PhysicsBodyType.DYNAMIC, + new Vector3f(9.0f, 8.0f, 7.0f), + new Quaternionf(), + new Vector3f(0.1f, 0.2f, 0.3f), + new Vector3f(0.4f, 0.5f, 0.6f), + 0.0f, + true)))); + } + + private static void writeHolderStorage(@Nonnull Store store, + @Nonnull List> holders) { + writeHolderStorageFile(PhysicsStoreHolderStorage.file(store.getExternalData()), + store, + holders); + } + + private static void writeHolderStorageFile(@Nonnull Path file, + @Nonnull Store store, + @Nonnull List> holders) { + BsonArray holderBlobs = new BsonArray(); + for (Holder holder : holders) { + holderBlobs.add(new BsonBinary(PhysicsStoreHolderPersistence.encodeHolder(store, + holder))); + } + BsonDocument document = new BsonDocument() + .append("SchemaVersion", new BsonInt32(1)) + .append("Holders", holderBlobs); + try { + Path parent = file.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + Files.write(file, BsonUtil.writeToBytes(document)); + } catch (IOException exception) { + throw new AssertionError("Failed to write test holder storage", exception); + } + } + + private static Holder holder(@Nonnull List> holders, + @Nonnull UUID uuid) { + for (Holder holder : holders) { + UuidComponent component = holder.getComponent(UuidComponent.getComponentType()); + if (component != null && uuid.equals(component.getUuid())) { + return holder; + } + } + return null; + } + + @Nonnull + private static List rowUuids(@Nonnull Store store) { + List uuids = new ArrayList<>(); + BiConsumer, CommandBuffer> collector = + (chunk, _) -> { + for (int index = 0; index < chunk.size(); index++) { + UuidComponent uuid = chunk.getComponent(index, UuidComponent.getComponentType()); + if (uuid != null) { + uuids.add(uuid.getUuid()); + } + } + }; + store.forEachChunk(UuidComponent.getComponentType(), collector); + return uuids; + } + + private static void setField(@Nonnull Object target, + @Nonnull Class owner, + @Nonnull String name, + @Nonnull Object value) { + try { + Field field = owner.getDeclaredField(name); + field.setAccessible(true); + field.set(target, value); + } catch (ReflectiveOperationException exception) { + throw new AssertionError("Failed to set test field " + owner.getName() + "." + name, + exception); + } + } + + @Nonnull + private static UUID uuid(long leastSignificantBits) { + return new UUID(0L, leastSignificantBits); + } + + private record StoreFixture(@Nonnull ComponentRegistry registry, + @Nonnull Store store) { + + private void close() { + if (!store.isShutdown()) { + registry.removeStore(store); + } + registry.shutdown(); + } + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/physics/resources/PhysicsDebugResourceTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/physics/resources/PhysicsDebugResourceTest.java new file mode 100644 index 00000000..ee50f85a --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/physics/resources/PhysicsDebugResourceTest.java @@ -0,0 +1,33 @@ +package dev.hytalemodding.impulse.core.internal.physics.resources; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.hytalemodding.impulse.core.internal.resources.PhysicsDebugResource; +import org.junit.jupiter.api.Test; + +class PhysicsDebugResourceTest { + + @Test + void contactsDefaultDisabledWhileOtherPhysicsFlagsRemainEnabled() { + PhysicsDebugResource resource = new PhysicsDebugResource(); + + assertTrue(resource.isDebugShapesEnabled()); + assertTrue(resource.isDebugMotionEnabled()); + assertFalse(resource.isDebugContactsEnabled()); + assertTrue(resource.isDebugJointsEnabled()); + assertFalse(resource.isDebugPhysicsChunkCollisionEnabled()); + } + + @Test + void clonePreservesExplicitPhysicsFlags() { + PhysicsDebugResource resource = new PhysicsDebugResource(); + resource.setDebugContactsEnabled(true); + resource.setDebugPhysicsChunkCollisionEnabled(true); + + PhysicsDebugResource copy = resource.clone(); + + assertTrue(copy.isDebugContactsEnabled()); + assertTrue(copy.isDebugPhysicsChunkCollisionEnabled()); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/registration/PhysicsTypeRegistrationApiTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/registration/PhysicsTypeRegistrationApiTest.java new file mode 100644 index 00000000..48433286 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/registration/PhysicsTypeRegistrationApiTest.java @@ -0,0 +1,63 @@ +package dev.hytalemodding.impulse.core.internal.registration; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.hypixel.hytale.component.ComponentRegistryProxy; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.plugin.components.PhysicsComponentTypes; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollision; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsEntityTypes; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class PhysicsTypeRegistrationApiTest { + + @Test + void internalRegistriesOwnRegistrationAndLifecycleWithoutPublicMutators() + throws NoSuchMethodException { + assertNotNull(PhysicsComponentTypeRegistry.class.getDeclaredMethod("registerComponentTypes", + ComponentRegistryProxy.class)); + assertNotNull(PhysicsResourceTypes.class.getDeclaredMethod("registerResourceTypes", + ComponentRegistryProxy.class)); + + assertFalse(hasPublicSetter(PhysicsComponentTypes.class)); + assertFalse(hasPublicSetter(PhysicsResourceTypes.class)); + assertFalse(hasPublicRegistrationMethod(PhysicsComponentTypes.class)); + assertFalse(hasPublicRegistrationMethod(PhysicsEntityTypes.class)); + assertFalse(hasPublicLifecycleMutator(PhysicsChunkCollision.class)); + } + + private static boolean hasPublicSetter(Class type) { + return Arrays.stream(type.getDeclaredMethods()) + .filter(method -> Modifier.isPublic(method.getModifiers())) + .map(Method::getName) + .anyMatch(name -> name.startsWith("set")); + } + + private static boolean hasPublicRegistrationMethod(Class type) { + return Arrays.stream(type.getDeclaredMethods()) + .filter(method -> Modifier.isPublic(method.getModifiers())) + .map(Method::getName) + .anyMatch(name -> name.startsWith("register") + || name.startsWith("unregister") + || name.startsWith("clear")); + } + + private static boolean hasPublicMethodNamed(Class type, String name) { + return Arrays.stream(type.getDeclaredMethods()) + .filter(method -> Modifier.isPublic(method.getModifiers())) + .map(Method::getName) + .anyMatch(name::equals); + } + + private static boolean hasPublicLifecycleMutator(Class type) { + return hasPublicMethodNamed(type, "enableModule") + || hasPublicMethodNamed(type, "disableModule") + || hasPublicMethodNamed(type, "enableSubPlugin") + || hasPublicMethodNamed(type, "disableSubPlugin"); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/BodyVisualInterestStateTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/BodyVisualInterestStateTest.java index 91fa59f2..a92a12bb 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/BodyVisualInterestStateTest.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/BodyVisualInterestStateTest.java @@ -4,11 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.ShapeType; import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime.BodyVisualInterestState; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.view.RaycastHitView; +import dev.hytalemodding.impulse.core.plugin.physics.RaycastHitView; import java.util.Optional; import java.util.concurrent.CompletableFuture; import org.joml.Vector3f; @@ -59,12 +56,9 @@ void freshnessCheckUsesAdvancedVisualTick() { void pendingRaycastResultsArePolledWithoutBlocking() { BodyVisualInterestState state = new BodyVisualInterestState(); CompletableFuture> pending = new CompletableFuture<>(); - RigidBodyKey bodyKey = RigidBodyKey.random(); - RaycastHitView hit = new RaycastHitView(bodyKey, - PhysicsBodyType.DYNAMIC, + RaycastHitView hit = new RaycastHitView(null, new Vector3f(), new Vector3f(0.0f, 1.0f, 0.0f), - ShapeType.BOX, 0.5f, 4.0f); diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsDebugOverlayResourceTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsDebugOverlayResourceTest.java new file mode 100644 index 00000000..28c5c8b5 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsDebugOverlayResourceTest.java @@ -0,0 +1,39 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.UUID; + +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources.PhysicsDebugOverlayResource; +import org.junit.jupiter.api.Test; + +class PhysicsDebugOverlayResourceTest { + + @Test + void clonePreservesSubscribersCadenceAndBudgets() { + UUID subscriberUuid = UUID.randomUUID(); + PhysicsDebugOverlayResource resource = new PhysicsDebugOverlayResource(); + resource.addSubscriber(subscriberUuid); + resource.setOverlayRefreshSeconds(0.25f); + resource.setPhysicsChunkRefreshSeconds(0.5f); + resource.setViewRadius(48.0); + resource.setMaxBodies(32); + resource.setMaxContacts(16); + resource.setMaxJoints(12); + resource.setMaxPhysicsChunkSections(8); + resource.setMaxPhysicsChunkBoxes(64); + + PhysicsDebugOverlayResource copy = resource.clone(); + + assertTrue(copy.getSubscriberUuids().contains(subscriberUuid)); + assertEquals(0.25f, copy.getOverlayRefreshSeconds()); + assertEquals(0.5f, copy.getPhysicsChunkRefreshSeconds()); + assertEquals(48.0, copy.getViewRadius()); + assertEquals(32, copy.getMaxBodies()); + assertEquals(16, copy.getMaxContacts()); + assertEquals(12, copy.getMaxJoints()); + assertEquals(8, copy.getMaxPhysicsChunkSections()); + assertEquals(64, copy.getMaxPhysicsChunkBoxes()); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsEntityProjectionResourcesTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsEntityProjectionResourcesTest.java new file mode 100644 index 00000000..6ed6e828 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsEntityProjectionResourcesTest.java @@ -0,0 +1,68 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Resource; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.PhysicsEntityTypeRegistry; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.resources.PhysicsBodySyncStateResource; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import java.util.ArrayList; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class PhysicsEntityProjectionResourcesTest { + + @AfterEach + void clearTypes() { + PhysicsEntityTypeRegistry.clearEntityStoreTypes(); + } + + @Test + void registersProjectionResourcesWithoutWorldResourceFacade() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + + PhysicsEntityTypeRegistry.registerComponentTypes(proxy); + PhysicsEntityTypeRegistry.registerResourceTypes(proxy); + PhysicsEntityTypeRegistry.registerEventTypes(proxy); + PhysicsEntityTypeRegistry.registerSystemGroups(proxy); + + assertResourceClass(PhysicsProjectionIndexResource.getResourceType(), + PhysicsProjectionIndexResource.class); + assertResourceClass(PhysicsBodySyncStateResource.getResourceType(), + PhysicsBodySyncStateResource.class); + assertResourceClass(PhysicsVisualInterestResource.getResourceType(), + PhysicsVisualInterestResource.class); + + Store store = registry.addStore(testEntityStore("projection-resources-test"), + EmptyResourceStorage.get()); + try { + assertNotNull(store.getResource(PhysicsProjectionIndexResource.getResourceType())); + assertNotNull(store.getResource(PhysicsBodySyncStateResource.getResourceType())); + assertNotNull(store.getResource(PhysicsVisualInterestResource.getResourceType())); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + private static > void assertResourceClass( + @Nonnull ResourceType resourceType, + @Nonnull Class expectedType) { + assertSame(expectedType, resourceType.getTypeClass()); + } + + @Nonnull + private static EntityStore testEntityStore(@Nonnull String worldName) { + return new EntityStore(TestInstanceFactory.world(worldName)); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsEventCollectionModeTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsEventCollectionModeTest.java index cf00c8cc..a33138b3 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsEventCollectionModeTest.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsEventCollectionModeTest.java @@ -3,7 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsEventCollectionMode; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventCollectionMode; import org.junit.jupiter.api.Test; class PhysicsEventCollectionModeTest { diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSpaceSettingsTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSpaceSettingsTest.java deleted file mode 100644 index a610303b..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsSpaceSettingsTest.java +++ /dev/null @@ -1,377 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.hypixel.hytale.codec.ExtraInfo; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsSpaceState; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsBackendExtensionId; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsCollisionLodSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsVisualMaterializationSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsVisualSyncSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldCollisionSettings; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicInteger; -import org.bson.BsonDocument; -import org.junit.jupiter.api.Test; - -class PhysicsSpaceSettingsTest { - - private static final AtomicInteger BACKEND_COUNTER = new AtomicInteger(); - - @Test - void defaultsExposeHeadlessVisualSyncRadii() { - PhysicsSpaceSettings settings = new PhysicsSpaceSettings(); - - assertEquals(PhysicsVisualSyncSettings.DEFAULT_VISUAL_FULL_SYNC_RADIUS, - settings.getVisualSyncSettings().getVisualFullSyncRadius()); - assertEquals(PhysicsVisualSyncSettings.DEFAULT_VISUAL_MAX_SYNC_RADIUS, - settings.getVisualSyncSettings().getVisualMaxSyncRadius()); - assertEquals(PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_INTEREST_REFRESH_INTERVAL_TICKS, - settings.getVisualMaterializationSettings().getDetachedVisualInterestRefreshIntervalTicks()); - assertEquals(PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_CANDIDATE_REFRESH_INTERVAL_TICKS, - settings.getVisualMaterializationSettings().getDetachedVisualCandidateRefreshIntervalTicks()); - assertEquals(PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_VISIBILITY_CHECK_INTERVAL_TICKS, - settings.getVisualMaterializationSettings().getDetachedVisualVisibilityCheckIntervalTicks()); - assertEquals(PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_NEAR_RADIUS, - settings.getCollisionLodSettings().getCollisionLodNearRadius()); - assertEquals(PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_MID_RADIUS, - settings.getCollisionLodSettings().getCollisionLodMidRadius()); - assertEquals(PhysicsCollisionLodSettings.DEFAULT_COLLISION_LOD_REFRESH_INTERVAL_TICKS, - settings.getCollisionLodSettings().getCollisionLodRefreshIntervalTicks()); - assertEquals(PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_PREDICTION_ENABLED, - settings.getVisualSyncSettings().isVisualSnapshotPredictionEnabled()); - assertEquals(PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_PREDICTION_MAX_SECONDS, - settings.getVisualSyncSettings().getVisualSnapshotPredictionMaxSeconds(), - 0.0001f); - assertEquals(PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_SMOOTHING_ENABLED, - settings.getVisualSyncSettings().isVisualSnapshotSmoothingEnabled()); - assertEquals(PhysicsVisualSyncSettings.DEFAULT_VISUAL_SNAPSHOT_SMOOTHING_RATE, - settings.getVisualSyncSettings().getVisualSnapshotSmoothingRate(), - 0.0001f); - } - - @Test - void rejectsVisualFullSyncRadiusAboveMaxRadius() { - PhysicsSpaceSettings settings = new PhysicsSpaceSettings(); - - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> settings.getVisualSyncSettings().setVisualFullSyncRadius(settings.getVisualSyncSettings().getVisualMaxSyncRadius() + 1)); - - assertEquals("Visual full sync radius cannot exceed visual max sync radius", - exception.getMessage()); - } - - @Test - void rejectsVisualMaxSyncRadiusBelowFullRadius() { - PhysicsSpaceSettings settings = new PhysicsSpaceSettings(); - - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> settings.getVisualSyncSettings().setVisualMaxSyncRadius(settings.getVisualSyncSettings().getVisualFullSyncRadius() - 1)); - - assertEquals("Visual max sync radius cannot be lower than visual full sync radius", - exception.getMessage()); - } - - @Test - void acceptsUpdatedVisualSyncRadiiWhenOrderingStaysValid() { - PhysicsSpaceSettings settings = new PhysicsSpaceSettings(); - - settings.getVisualSyncSettings().setVisualMaxSyncRadius(192); - settings.getVisualSyncSettings().setVisualFullSyncRadius(96); - - assertEquals(192, settings.getVisualSyncSettings().getVisualMaxSyncRadius()); - assertEquals(96, settings.getVisualSyncSettings().getVisualFullSyncRadius()); - } - - @Test - void rejectsNonPositiveWorldCollisionValues() { - PhysicsSpaceSettings settings = new PhysicsSpaceSettings(); - - assertEquals("World collision radius must be between 1 and " - + PhysicsWorldCollisionSettings.MAX_WORLD_COLLISION_RADIUS, - assertThrows(IllegalArgumentException.class, - () -> settings.getWorldCollisionSettings().setWorldCollisionRadius(0)).getMessage()); - assertEquals("World collision body radius must be between 1 and " - + PhysicsWorldCollisionSettings.MAX_WORLD_COLLISION_BODY_RADIUS, - assertThrows(IllegalArgumentException.class, - () -> settings.getWorldCollisionSettings().setWorldCollisionBodyRadius(0)).getMessage()); - assertEquals("World collision TTL must be between 1 and " - + PhysicsWorldCollisionSettings.MAX_WORLD_COLLISION_TTL_TICKS, - assertThrows(IllegalArgumentException.class, - () -> settings.getWorldCollisionSettings().setWorldCollisionTtlTicks(0)).getMessage()); - assertEquals("Terrain friction must be finite and >= 0.0", - assertThrows(IllegalArgumentException.class, - () -> settings.getWorldCollisionSettings().setTerrainFriction(-0.1f)).getMessage()); - assertEquals("Terrain restitution must be finite and >= 0.0", - assertThrows(IllegalArgumentException.class, - () -> settings.getWorldCollisionSettings().setTerrainRestitution(Float.NaN)).getMessage()); - assertEquals("Visual full sync radius must be between 1 and " - + PhysicsVisualSyncSettings.MAX_VISUAL_FULL_SYNC_RADIUS, - assertThrows(IllegalArgumentException.class, - () -> settings.getVisualSyncSettings().setVisualFullSyncRadius(0)).getMessage()); - assertEquals("Visual max sync radius must be between 1 and " - + PhysicsVisualSyncSettings.MAX_VISUAL_MAX_SYNC_RADIUS, - assertThrows(IllegalArgumentException.class, - () -> settings.getVisualSyncSettings().setVisualMaxSyncRadius(0)).getMessage()); - assertEquals("Detached visual interest refresh interval must be between 1 and " - + PhysicsVisualMaterializationSettings.MAX_DETACHED_VISUAL_CACHE_INTERVAL_TICKS, - assertThrows(IllegalArgumentException.class, - () -> settings.getVisualMaterializationSettings().setDetachedVisualInterestRefreshIntervalTicks(0)).getMessage()); - assertEquals("Detached visual candidate refresh interval must be between 1 and " - + PhysicsVisualMaterializationSettings.MAX_DETACHED_VISUAL_CACHE_INTERVAL_TICKS, - assertThrows(IllegalArgumentException.class, - () -> settings.getVisualMaterializationSettings().setDetachedVisualCandidateRefreshIntervalTicks(0)).getMessage()); - assertEquals("Detached visual visibility check interval must be between 1 and " - + PhysicsVisualMaterializationSettings.MAX_DETACHED_VISUAL_CACHE_INTERVAL_TICKS, - assertThrows(IllegalArgumentException.class, - () -> settings.getVisualMaterializationSettings().setDetachedVisualVisibilityCheckIntervalTicks(0)).getMessage()); - assertEquals("Collision LOD near radius must be between 1 and " - + PhysicsCollisionLodSettings.MAX_COLLISION_LOD_RADIUS, - assertThrows(IllegalArgumentException.class, - () -> settings.getCollisionLodSettings().setCollisionLodNearRadius(0)).getMessage()); - assertEquals("Collision LOD mid radius must be between 1 and " - + PhysicsCollisionLodSettings.MAX_COLLISION_LOD_RADIUS, - assertThrows(IllegalArgumentException.class, - () -> settings.getCollisionLodSettings().setCollisionLodMidRadius(0)).getMessage()); - assertEquals("Collision LOD refresh interval must be between 1 and " - + PhysicsCollisionLodSettings.MAX_COLLISION_LOD_REFRESH_INTERVAL_TICKS, - assertThrows(IllegalArgumentException.class, - () -> settings.getCollisionLodSettings().setCollisionLodRefreshIntervalTicks(0)).getMessage()); - assertEquals("Visual snapshot prediction max seconds must be between 0 and " - + PhysicsVisualSyncSettings.MAX_VISUAL_SNAPSHOT_PREDICTION_MAX_SECONDS, - assertThrows(IllegalArgumentException.class, - () -> settings.getVisualSyncSettings().setVisualSnapshotPredictionMaxSeconds( - PhysicsVisualSyncSettings.MAX_VISUAL_SNAPSHOT_PREDICTION_MAX_SECONDS - + 0.01f)).getMessage()); - assertEquals("Visual snapshot smoothing rate must be > 0 and <= " - + PhysicsVisualSyncSettings.MAX_VISUAL_SNAPSHOT_SMOOTHING_RATE, - assertThrows(IllegalArgumentException.class, - () -> settings.getVisualSyncSettings().setVisualSnapshotSmoothingRate(0.0f)).getMessage()); - } - - @Test - void rejectsInvalidCollisionLodOrderingAndHysteresis() { - PhysicsSpaceSettings settings = new PhysicsSpaceSettings(); - - IllegalArgumentException radiusException = assertThrows(IllegalArgumentException.class, - () -> settings.getCollisionLodSettings().setCollisionLodRadii(96, 64)); - IllegalArgumentException hysteresisException = assertThrows(IllegalArgumentException.class, - () -> settings.getCollisionLodSettings().setCollisionLodHysteresis( - PhysicsCollisionLodSettings.MAX_COLLISION_LOD_HYSTERESIS + 1)); - - assertEquals("Collision LOD near radius cannot exceed mid radius", - radiusException.getMessage()); - assertEquals("Collision LOD hysteresis must be between 0 and " - + PhysicsCollisionLodSettings.MAX_COLLISION_LOD_HYSTERESIS, - hysteresisException.getMessage()); - } - - @Test - void defaultsFactoryReturnsFreshDefaultSettings() { - PhysicsSpaceSettings first = PhysicsSpaceSettings.defaults(); - PhysicsSpaceSettings second = PhysicsSpaceSettings.defaults(); - - assertNotSame(first, second); - assertEquals(WorldCollisionMode.NONE, first.getWorldCollisionSettings().getWorldCollisionMode()); - assertSame(PhysicsWorldCollisionSettings.DEFAULT_ENTITY_CHUNK_BOUNDARY_MODE, - first.getWorldCollisionSettings().getEntityChunkBoundaryMode()); - assertFalse(first.getWorldCollisionSettings().isNativeVoxelTerrainEnabled()); - assertEquals(PhysicsWorldCollisionSettings.DEFAULT_TERRAIN_FRICTION, - first.getWorldCollisionSettings().getTerrainFriction(), - 0.0001f); - assertEquals(PhysicsWorldCollisionSettings.DEFAULT_TERRAIN_RESTITUTION, - first.getWorldCollisionSettings().getTerrainRestitution(), - 0.0001f); - } - - @Test - void groupedAccessorsExposeIndependentDomainState() { - PhysicsSpaceSettings settings = new PhysicsSpaceSettings(); - - settings.getWorldCollisionSettings().setWorldCollisionRadius(14); - settings.getVisualSyncSettings().setVisualSyncRadii(36, 144); - settings.getSolverSettings().setSolverIterations(6); - settings.getVisualMaterializationSettings().setDetachedVisualMaxMaterialized(96); - settings.getCollisionLodSettings().setCollisionLodRadii(24, 72); - - assertEquals(14, settings.getWorldCollisionSettings().getWorldCollisionRadius()); - assertEquals(36, settings.getVisualSyncSettings().getVisualFullSyncRadius()); - assertEquals(144, settings.getVisualSyncSettings().getVisualMaxSyncRadius()); - assertEquals(6, settings.getSolverSettings().getSolverIterations()); - assertEquals(96, settings.getVisualMaterializationSettings().getDetachedVisualMaxMaterialized()); - assertEquals(24, settings.getCollisionLodSettings().getCollisionLodNearRadius()); - assertEquals(72, settings.getCollisionLodSettings().getCollisionLodMidRadius()); - - settings.getWorldCollisionSettings().setWorldCollisionBodyRadius(5); - settings.getVisualSyncSettings().setVisualMidSyncIntervalTicks(3); - settings.getSolverSettings().setDynamicSleepLinearThreshold(0.45f); - settings.getVisualMaterializationSettings().setDetachedVisualMaxSpawnsPerTick(16); - settings.getCollisionLodSettings().setCollisionLodHysteresis(4); - - assertEquals(5, settings.getWorldCollisionSettings().getWorldCollisionBodyRadius()); - assertEquals(3, settings.getVisualSyncSettings().getVisualMidSyncIntervalTicks()); - assertEquals(0.45f, settings.getSolverSettings().getDynamicSleepLinearThreshold(), 0.0001f); - assertEquals(16, - settings.getVisualMaterializationSettings().getDetachedVisualMaxSpawnsPerTick()); - assertEquals(4, settings.getCollisionLodSettings().getCollisionLodHysteresis()); - } - - @Test - void extensionSettingsAreTypedAndCopyIsolated() { - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - PhysicsBackendExtensionId extensionId = new PhysicsBackendExtensionId("test:extension"); - - settings.getExtensionSettings().setInt(extensionId, "iterations", 7); - settings.getExtensionSettings().setFloat(extensionId, "scale", 1.5f); - settings.getExtensionSettings().setBoolean(extensionId, "enabled", true); - settings.getExtensionSettings().setString(extensionId, "mode", "stable"); - - PhysicsSpaceSettings copy = new PhysicsSpaceSettings(settings); - settings.getExtensionSettings().setInt(extensionId, "iterations", 11); - copy.getExtensionSettings().setString(extensionId, "mode", "copy"); - - assertEquals(11, settings.getExtensionSettings().getInt(extensionId, "iterations").orElseThrow()); - assertEquals("stable", settings.getExtensionSettings().getString(extensionId, "mode").orElseThrow()); - assertEquals(7, copy.getExtensionSettings().getInt(extensionId, "iterations").orElseThrow()); - assertEquals(1.5f, copy.getExtensionSettings().getFloat(extensionId, "scale").orElseThrow(), 0.0001f); - assertTrue(copy.getExtensionSettings().getBoolean(extensionId, "enabled").orElseThrow()); - assertEquals("copy", copy.getExtensionSettings().getString(extensionId, "mode").orElseThrow()); - } - - @Test - void defaultsDoNotCarryBackendExtensionValues() { - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - - assertTrue(settings.getExtensionSettings().isEmpty()); - } - - @Test - void streamingWorldCollisionFactoryEnablesStreamingMode() { - PhysicsSpaceSettings settings = PhysicsSpaceSettings.streamingWorldCollision(); - - assertEquals(WorldCollisionMode.STREAMING, settings.getWorldCollisionSettings().getWorldCollisionMode()); - assertEquals(PhysicsWorldCollisionSettings.DEFAULT_WORLD_COLLISION_RADIUS, - settings.getWorldCollisionSettings().getWorldCollisionRadius()); - } - - @Test - void copyConstructorCopiesValuesWithoutSharingOriginalInstance() { - PhysicsSpaceSettings original = new PhysicsSpaceSettings(); - original.getWorldCollisionSettings().setWorldCollisionMode(WorldCollisionMode.STREAMING); - original.getWorldCollisionSettings().setWorldCollisionRadius(12); - original.getWorldCollisionSettings().setWorldCollisionBodyRadius(6); - original.getWorldCollisionSettings().setWorldCollisionTtlTicks(180); - original.getWorldCollisionSettings().setNativeVoxelTerrainEnabled(true); - original.getWorldCollisionSettings().setTerrainMaterial(0.9f, 0.15f); - original.getVisualSyncSettings().setVisualMaxSyncRadius(160); - original.getVisualSyncSettings().setVisualFullSyncRadius(80); - original.getVisualMaterializationSettings().setDetachedVisualInterestRefreshIntervalTicks(2); - original.getVisualMaterializationSettings().setDetachedVisualCandidateRefreshIntervalTicks(3); - original.getVisualMaterializationSettings().setDetachedVisualVisibilityCheckIntervalTicks(12); - original.getVisualSyncSettings().setVisualSnapshotPredictionEnabled(true); - original.getVisualSyncSettings().setVisualSnapshotPredictionMaxSeconds(0.08f); - original.getVisualSyncSettings().setVisualSnapshotSmoothingEnabled(true); - original.getVisualSyncSettings().setVisualSnapshotSmoothingRate(18.0f); - original.getCollisionLodSettings().setCollisionLodEnabled(true); - original.getCollisionLodSettings().setCollisionLodRadii(32, 96); - original.getCollisionLodSettings().setCollisionLodHysteresis(8); - original.getCollisionLodSettings().setCollisionLodRefreshIntervalTicks(6); - original.getCollisionLodSettings().setCollisionLodFarSleepEnabled(false); - - PhysicsSpaceSettings copy = new PhysicsSpaceSettings(original); - original.getWorldCollisionSettings().setWorldCollisionRadius(20); - original.getVisualSyncSettings().setVisualSyncRadii(96, 192); - original.getVisualMaterializationSettings().setDetachedVisualMaxMaterialized(128); - original.getCollisionLodSettings().setCollisionLodRadii(48, 112); - - assertNotSame(original.getWorldCollisionSettings(), copy.getWorldCollisionSettings()); - assertNotSame(original.getVisualSyncSettings(), copy.getVisualSyncSettings()); - assertNotSame(original.getSolverSettings(), copy.getSolverSettings()); - assertNotSame(original.getVisualMaterializationSettings(), - copy.getVisualMaterializationSettings()); - assertNotSame(original.getCollisionLodSettings(), copy.getCollisionLodSettings()); - assertEquals(WorldCollisionMode.STREAMING, copy.getWorldCollisionSettings().getWorldCollisionMode()); - assertEquals(12, copy.getWorldCollisionSettings().getWorldCollisionRadius()); - assertEquals(6, copy.getWorldCollisionSettings().getWorldCollisionBodyRadius()); - assertEquals(180, copy.getWorldCollisionSettings().getWorldCollisionTtlTicks()); - assertTrue(copy.getWorldCollisionSettings().isNativeVoxelTerrainEnabled()); - assertEquals(0.9f, copy.getWorldCollisionSettings().getTerrainFriction(), 0.0001f); - assertEquals(0.15f, copy.getWorldCollisionSettings().getTerrainRestitution(), 0.0001f); - assertEquals(160, copy.getVisualSyncSettings().getVisualMaxSyncRadius()); - assertEquals(80, copy.getVisualSyncSettings().getVisualFullSyncRadius()); - assertEquals(2, copy.getVisualMaterializationSettings().getDetachedVisualInterestRefreshIntervalTicks()); - assertEquals(3, copy.getVisualMaterializationSettings().getDetachedVisualCandidateRefreshIntervalTicks()); - assertEquals(12, copy.getVisualMaterializationSettings().getDetachedVisualVisibilityCheckIntervalTicks()); - assertTrue(copy.getVisualSyncSettings().isVisualSnapshotPredictionEnabled()); - assertEquals(0.08f, copy.getVisualSyncSettings().getVisualSnapshotPredictionMaxSeconds(), 0.0001f); - assertTrue(copy.getVisualSyncSettings().isVisualSnapshotSmoothingEnabled()); - assertEquals(18.0f, copy.getVisualSyncSettings().getVisualSnapshotSmoothingRate(), 0.0001f); - assertTrue(copy.getCollisionLodSettings().isCollisionLodEnabled()); - assertEquals(32, copy.getCollisionLodSettings().getCollisionLodNearRadius()); - assertEquals(96, copy.getCollisionLodSettings().getCollisionLodMidRadius()); - assertEquals(8, copy.getCollisionLodSettings().getCollisionLodHysteresis()); - assertEquals(6, copy.getCollisionLodSettings().getCollisionLodRefreshIntervalTicks()); - assertFalse(copy.getCollisionLodSettings().isCollisionLodFarSleepEnabled()); - } - - @Test - void persistentSpaceStateRoundTripPreservesDetachedVisualCadenceSettings() { - PhysicsSpaceSettings original = PhysicsSpaceSettings.defaults(); - original.getWorldCollisionSettings().setNativeVoxelTerrainEnabled(true); - original.getWorldCollisionSettings().setTerrainMaterial(0.85f, 0.2f); - original.getVisualMaterializationSettings().setDetachedVisualInterestRefreshIntervalTicks(7); - original.getVisualMaterializationSettings().setDetachedVisualCandidateRefreshIntervalTicks(9); - original.getVisualMaterializationSettings().setDetachedVisualVisibilityCheckIntervalTicks(11); - - FakePhysicsBackend backend = - new FakePhysicsBackend("test:settings-persistence-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), "test-world", original); - PersistentPhysicsSpaceState state = - PersistentPhysicsSpaceState.from(resource.requireSpaceBinding(space.id()), original); - - BsonDocument encoded = PersistentPhysicsSpaceState.CODEC.encode(state, new ExtraInfo()).asDocument(); - - assertTrue(encoded.containsKey("NativeVoxelTerrainEnabled")); - assertTrue(encoded.containsKey("TerrainFriction")); - assertTrue(encoded.containsKey("TerrainRestitution")); - assertTrue(encoded.containsKey("DetachedVisualInterestRefreshIntervalTicks")); - assertTrue(encoded.containsKey("DetachedVisualCandidateRefreshIntervalTicks")); - assertTrue(encoded.containsKey("DetachedVisualVisibilityCheckIntervalTicks")); - PhysicsSpaceSettings decoded = Objects.requireNonNull( - PersistentPhysicsSpaceState.CODEC.decode(encoded, new ExtraInfo())).toSettings(); - assertTrue(decoded.getWorldCollisionSettings().isNativeVoxelTerrainEnabled()); - assertEquals(0.85f, decoded.getWorldCollisionSettings().getTerrainFriction(), 0.0001f); - assertEquals(0.2f, decoded.getWorldCollisionSettings().getTerrainRestitution(), 0.0001f); - assertDetachedVisualCadence(decoded, - 7, - 9, - 11); - PhysicsSpaceSettings copied = state.copy().toSettings(); - assertTrue(copied.getWorldCollisionSettings().isNativeVoxelTerrainEnabled()); - assertEquals(0.85f, copied.getWorldCollisionSettings().getTerrainFriction(), 0.0001f); - assertEquals(0.2f, copied.getWorldCollisionSettings().getTerrainRestitution(), 0.0001f); - assertDetachedVisualCadence(copied, 7, 9, 11); - } - - private static void assertDetachedVisualCadence(PhysicsSpaceSettings settings, - int interestInterval, - int candidateInterval, - int visibilityInterval) { - assertEquals(interestInterval, settings.getVisualMaterializationSettings().getDetachedVisualInterestRefreshIntervalTicks()); - assertEquals(candidateInterval, settings.getVisualMaterializationSettings().getDetachedVisualCandidateRefreshIntervalTicks()); - assertEquals(visibilityInterval, settings.getVisualMaterializationSettings().getDetachedVisualVisibilityCheckIntervalTicks()); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStepSchedulerResourceTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStepSchedulerResourceTest.java new file mode 100644 index 00000000..ee1f85f5 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStepSchedulerResourceTest.java @@ -0,0 +1,228 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.hytalemodding.impulse.api.PhysicsStepPhaseStats; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepSchedulingMode; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class PhysicsStepSchedulerResourceTest { + + @Test + void completedStepCarriesCompactSnapshotPayloadInsteadOfBodySnapshotList() { + assertTrue(Arrays.stream(PhysicsStepSchedulerResource.CompletedStep.class.getRecordComponents()) + .map(component -> component.getGenericType().getTypeName()) + .noneMatch(typeName -> typeName.contains( + "dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot")), + "CompletedStep should not expose List in the owner-lane handoff"); + assertTrue(Arrays.stream(PhysicsStepSchedulerResource.CompletedStep.class.getDeclaredMethods()) + .noneMatch(method -> method.getName().equals("bodySnapshots")), + "CompletedStep should expose compact snapshot payloads, not bodySnapshots()"); + } + + @Test + void submittedStepRunsAsynchronouslyAndSkipsWhilePending() throws Exception { + PhysicsStepSchedulerResource scheduler = new PhysicsStepSchedulerResource(); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + + PhysicsStepSchedulerResource.StepInput input = scheduler.acceptStepInput(0.05f, + PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT, + 0.10f); + assertTrue(scheduler.submitStep(input, () -> { + entered.countDown(); + awaitOrFail(release); + return new PhysicsStepSchedulerResource.CompletedStep(1, + 2, + 123L, + PhysicsStepPhaseStats.unavailable()); + }, 10L)); + + assertTrue(entered.await(5, TimeUnit.SECONDS)); + assertTrue(scheduler.isStepPending()); + + PhysicsStepSchedulerResource.TickDecision skipped = scheduler.beforeStoreTick(0.05f, + PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT, + 0.10f, + 20L); + assertFalse(skipped.shouldTick()); + assertEquals(10L, skipped.pendingStepAgeNanos()); + assertEquals(0.05f, skipped.backlogDtSeconds(), 0.0001f); + assertNull(scheduler.pollCompletedStep()); + + release.countDown(); + PhysicsStepSchedulerResource.TickDecision allowed = awaitAllowedTick(scheduler); + assertTrue(allowed.shouldTick()); + + PhysicsStepSchedulerResource.CompletedStep completed = scheduler.pollCompletedStep(); + assertNotNull(completed); + assertEquals(1, completed.spaces()); + assertEquals(2, completed.substeps()); + assertEquals(123L, completed.stepSubmitNanos()); + assertEquals(0.05f, completed.input().submittedDtSeconds(), 0.0001f); + assertFalse(scheduler.isStepPending()); + scheduler.close(); + } + + @Test + void dropPendingDtDoesNotCatchUpWithLargePostSkipDt() throws Exception { + PhysicsStepSchedulerResource scheduler = new PhysicsStepSchedulerResource(); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + + PhysicsStepSchedulerResource.StepInput input = scheduler.acceptStepInput(0.05f, + PhysicsStepSchedulingMode.DROP_PENDING_DT, + 0.50f); + assertEquals(0.05f, input.submittedDtSeconds(), 0.0001f); + assertTrue(scheduler.submitStep(input, () -> { + entered.countDown(); + awaitOrFail(release); + return new PhysicsStepSchedulerResource.CompletedStep(1, + 1, + 10L, + PhysicsStepPhaseStats.unavailable()); + }, 10L)); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + + PhysicsStepSchedulerResource.TickDecision skipped = scheduler.beforeStoreTick(0.05f, + PhysicsStepSchedulingMode.DROP_PENDING_DT, + 0.50f, + 20L); + assertFalse(skipped.shouldTick()); + assertEquals(0.05f, skipped.droppedBacklogDtSeconds(), 0.0001f); + + release.countDown(); + scheduler.whenIdle().toCompletableFuture().get(5, TimeUnit.SECONDS); + PhysicsStepSchedulerResource.TickDecision allowed = scheduler.beforeStoreTick(0.20f, + PhysicsStepSchedulingMode.DROP_PENDING_DT, + 0.50f, + 30L); + assertTrue(allowed.shouldTick()); + + PhysicsStepSchedulerResource.StepInput postSkipInput = scheduler.acceptStepInput(0.20f, + PhysicsStepSchedulingMode.DROP_PENDING_DT, + 0.50f); + assertEquals(0.20f, postSkipInput.inputDtSeconds(), 0.0001f); + assertEquals(0.05f, postSkipInput.submittedDtSeconds(), 0.0001f); + assertEquals(0.0f, postSkipInput.backlogDtSeconds(), 0.0001f); + assertEquals(0.15f, postSkipInput.droppedBacklogDtSeconds(), 0.0001f); + assertFalse(postSkipInput.dtCapHit()); + scheduler.close(); + } + + @Test + void accumulatePendingDtStillCatchesUpAfterPendingSkip() throws Exception { + PhysicsStepSchedulerResource scheduler = new PhysicsStepSchedulerResource(); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + + PhysicsStepSchedulerResource.StepInput input = scheduler.acceptStepInput(0.05f, + PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT, + 0.50f); + assertTrue(scheduler.submitStep(input, () -> { + entered.countDown(); + awaitOrFail(release); + return new PhysicsStepSchedulerResource.CompletedStep(1, + 1, + 10L, + PhysicsStepPhaseStats.unavailable()); + }, 10L)); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + + PhysicsStepSchedulerResource.TickDecision skipped = scheduler.beforeStoreTick(0.05f, + PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT, + 0.50f, + 20L); + assertFalse(skipped.shouldTick()); + assertEquals(0.05f, skipped.backlogDtSeconds(), 0.0001f); + + release.countDown(); + scheduler.whenIdle().toCompletableFuture().get(5, TimeUnit.SECONDS); + PhysicsStepSchedulerResource.TickDecision allowed = scheduler.beforeStoreTick(0.20f, + PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT, + 0.50f, + 30L); + assertTrue(allowed.shouldTick()); + + PhysicsStepSchedulerResource.StepInput postSkipInput = scheduler.acceptStepInput(0.20f, + PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT, + 0.50f); + assertEquals(0.20f, postSkipInput.inputDtSeconds(), 0.0001f); + assertEquals(0.25f, postSkipInput.submittedDtSeconds(), 0.0001f); + assertEquals(0.0f, postSkipInput.backlogDtSeconds(), 0.0001f); + assertEquals(0.0f, postSkipInput.droppedBacklogDtSeconds(), 0.0001f); + assertFalse(postSkipInput.dtCapHit()); + scheduler.close(); + } + + @Test + void whenIdleCompletesAfterPendingStepFinishes() throws Exception { + PhysicsStepSchedulerResource scheduler = new PhysicsStepSchedulerResource(); + CountDownLatch entered = new CountDownLatch(1); + CountDownLatch release = new CountDownLatch(1); + + PhysicsStepSchedulerResource.StepInput input = scheduler.acceptStepInput(0.05f, + PhysicsStepSchedulingMode.DROP_PENDING_DT, + 0.10f); + assertTrue(scheduler.submitStep(input, () -> { + entered.countDown(); + awaitOrFail(release); + return new PhysicsStepSchedulerResource.CompletedStep(1, + 1, + 10L, + PhysicsStepPhaseStats.unavailable()); + }, 10L)); + assertTrue(entered.await(5, TimeUnit.SECONDS)); + + CompletableFuture idle = scheduler.whenIdle().toCompletableFuture(); + assertFalse(idle.isDone()); + + release.countDown(); + idle.get(5, TimeUnit.SECONDS); + assertFalse(scheduler.isStepPending()); + scheduler.close(); + } + + private static PhysicsStepSchedulerResource.TickDecision awaitAllowedTick( + PhysicsStepSchedulerResource scheduler) throws InterruptedException { + return awaitAllowedTick(scheduler, + PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT, + 0.05f, + 0.10f); + } + + private static PhysicsStepSchedulerResource.TickDecision awaitAllowedTick( + PhysicsStepSchedulerResource scheduler, + PhysicsStepSchedulingMode mode, + float dt, + float maxSubmittedDt) throws InterruptedException { + for (int attempt = 0; attempt < 50; attempt++) { + PhysicsStepSchedulerResource.TickDecision decision = scheduler.beforeStoreTick(dt, + mode, + maxSubmittedDt, + 30L + attempt); + if (decision.shouldTick()) { + return decision; + } + Thread.sleep(10L); + } + throw new AssertionError("Scheduler did not allow the tick after step completion"); + } + + private static void awaitOrFail(CountDownLatch latch) { + try { + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } catch (InterruptedException exception) { + Thread.currentThread().interrupt(); + throw new AssertionError("Interrupted while waiting for scheduler test latch", exception); + } + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStepSchedulingModeTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStepSchedulingModeTest.java index 8d7e181b..bd2c5cce 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStepSchedulingModeTest.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStepSchedulingModeTest.java @@ -26,7 +26,7 @@ void rejectsUnknownSerializedNames() { @Test void describesPendingStepBehavior() { - assertEquals("drop dt while an owner step is pending", + assertEquals("drop pending dt and prevent post-skip catch-up", PhysicsStepSchedulingMode.DROP_PENDING_DT.describePendingStepBehavior()); assertEquals("accumulate pending dt for one capped catch-up step", PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT.describePendingStepBehavior()); diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStoreResourceIndexTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStoreResourceIndexTest.java new file mode 100644 index 00000000..2bf6cc3c --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsStoreResourceIndexTest.java @@ -0,0 +1,624 @@ +package dev.hytalemodding.impulse.core.internal.resources; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionMutation; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.ChunkCollisionPayload; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendJointHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkCollisionMutationQueueResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsSnapshotFrame; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class PhysicsStoreResourceIndexTest { + + @Test + void compatibilityIndexMaintainsBothDirectionsWhenMappingsMove() { + PhysicsSpaceCompatibilityIndexResource index = new PhysicsSpaceCompatibilityIndexResource(); + UUID firstSpaceUuid = UUID.fromString("00000000-0000-0000-0000-000000000001"); + UUID secondSpaceUuid = UUID.fromString("00000000-0000-0000-0000-000000000002"); + + index.putSpace(new SpaceId(7), firstSpaceUuid); + index.putSpace(new SpaceId(8), firstSpaceUuid); + index.putSpace(new SpaceId(8), secondSpaceUuid); + + assertNull(index.getSpaceUuid(new SpaceId(7))); + assertNull(index.getSpaceId(firstSpaceUuid)); + assertEquals(secondSpaceUuid, index.getSpaceUuid(new SpaceId(8))); + assertEquals(new SpaceId(8), index.getSpaceId(secondSpaceUuid)); + assertEquals(List.of(new SpaceId(8)), List.copyOf(index.spaceIds())); + } + + @Test + void runtimeIndexesKeepRefHandlesAndScopedBackendMetadataTogether() { + PhysicsRuntimeResource runtime = new PhysicsRuntimeResource(); + BackendId backendId = new BackendId("test:runtime-index"); + PhysicsBackendRuntime backendRuntime = + new FakePhysicsBackendRuntimeProvider(backendId, false, false).createRuntime(); + BackendId otherBackendId = new BackendId("test:runtime-index-other"); + PhysicsBackendRuntime otherBackendRuntime = + new FakePhysicsBackendRuntimeProvider(otherBackendId, false, false).createRuntime(); + UUID spaceUuid = UUID.fromString("00000000-0000-0000-0000-000000000003"); + UUID collidingSpaceUuid = UUID.fromString("00000000-0000-0000-0000-000000000013"); + UUID bodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000004"); + BackendSpaceHandle oldSpaceHandle = new BackendSpaceHandle(30); + BackendSpaceHandle spaceHandle = new BackendSpaceHandle(31); + BackendBodyHandle bodyHandle = new BackendBodyHandle(42L); + Ref spaceRef = new TestRef(1); + Ref collidingSpaceRef = new TestRef(13); + Ref bodyRef = new TestRef(2); + + runtime.putRuntime(backendId, backendRuntime); + runtime.putRuntime(otherBackendId, otherBackendRuntime); + runtime.putSpaceHandle(spaceRef, backendId, oldSpaceHandle); + runtime.putSpaceMetadata(backendId, oldSpaceHandle, spaceUuid, spaceRef); + + runtime.putSpaceHandle(spaceRef, backendId, spaceHandle); + runtime.putSpaceMetadata(backendId, spaceHandle, spaceUuid, spaceRef); + assertSame(backendRuntime, runtime.runtimeForSpaceRef(spaceRef)); + + runtime.putSpaceHandle(collidingSpaceRef, otherBackendId, spaceHandle); + runtime.putSpaceMetadata(otherBackendId, spaceHandle, collidingSpaceUuid, collidingSpaceRef); + assertSame(backendRuntime, runtime.runtimeForSpaceRef(spaceRef)); + assertSame(otherBackendRuntime, runtime.runtimeForSpaceRef(collidingSpaceRef)); + runtime.removeSpaceHandle(collidingSpaceRef); + + runtime.putBodyHandle(bodyRef, spaceRef, spaceHandle, bodyHandle); + runtime.putBodySnapshotMetadata(backendId, + spaceHandle, + bodyHandle, + bodyUuid, + bodyRef, + spaceUuid); + runtime.putBodyHitMetadata(backendId, + spaceHandle, + bodyHandle, + bodyUuid, + bodyRef, + PhysicsBodyType.DYNAMIC, + ShapeType.BOX); + + assertSame(backendRuntime, runtime.getRuntime(backendId)); + assertEquals(spaceUuid, runtime.getSpaceUuid(spaceRef)); + assertEquals(spaceHandle, runtime.getSpaceHandle(spaceRef)); + assertEquals(backendId, runtime.getSpaceBackendId(spaceRef)); + assertEquals(bodyHandle, runtime.getBodyHandle(bodyRef)); + assertEquals(spaceHandle, runtime.getBodySpaceHandle(bodyRef)); + assertSame(backendRuntime, runtime.runtimeForBodyRef(bodyRef)); + assertEquals(bodyUuid, + runtime.getBodySnapshotMetadata(backendId, spaceHandle, bodyHandle.value()).bodyUuid()); + + List handles = new ArrayList<>(); + runtime.forEachBodyHandle(backendId, spaceHandle, handles::add); + assertEquals(List.of(bodyHandle.value()), handles); + + runtime.removeBodyHandle(bodyRef); + + assertNull(runtime.getBodyHandle(bodyRef)); + assertNull(runtime.getBodySpaceHandle(bodyRef)); + assertNull(runtime.runtimeForBodyRef(bodyRef)); + assertNull(runtime.getBodySnapshotMetadata(backendId, spaceHandle, bodyHandle.value())); + assertNull(runtime.getBodyHitMetadata(backendId, spaceHandle, bodyHandle.value())); + handles.clear(); + runtime.forEachBodyHandle(backendId, spaceHandle, handles::add); + assertEquals(List.of(), handles); + + runtime.removeSpaceHandle(spaceRef); + + assertNull(runtime.getSpaceHandle(spaceRef)); + } + + @Test + void runtimeIndexesExposeRefsForTopologyCleanup() { + PhysicsRuntimeResource runtime = new PhysicsRuntimeResource(); + UUID spaceUuid = UUID.fromString("00000000-0000-0000-0000-000000000007"); + UUID bodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000008"); + UUID jointUuid = UUID.fromString("00000000-0000-0000-0000-000000000009"); + BackendId backendId = new BackendId("test:runtime-ref-index"); + PhysicsBackendRuntime backendRuntime = + new FakePhysicsBackendRuntimeProvider(backendId, false, false).createRuntime(); + BackendSpaceHandle spaceHandle = new BackendSpaceHandle(43); + BackendBodyHandle bodyHandle = new BackendBodyHandle(44L); + BackendJointHandle jointHandle = new BackendJointHandle(45L); + BackendJointHandle reboundJointHandle = new BackendJointHandle(46L); + Ref spaceRef = new TestRef(3); + Ref bodyRef = new TestRef(4); + Ref jointRef = new TestRef(5); + Ref reboundJointRef = new TestRef(6); + + runtime.putRuntime(backendId, backendRuntime); + runtime.putSpaceHandle(spaceRef, backendId, spaceHandle); + runtime.putSpaceMetadata(backendId, spaceHandle, spaceUuid, spaceRef); + runtime.putBodyHandle(bodyRef, spaceRef, spaceHandle, bodyHandle); + runtime.putBodySnapshotMetadata(backendId, + spaceHandle, + bodyHandle, + bodyUuid, + bodyRef, + spaceUuid); + runtime.putJointHandle(jointRef, spaceRef, spaceHandle, jointHandle); + runtime.putJointMetadata(backendId, spaceHandle, jointHandle, jointUuid, jointRef); + + assertEquals(List.of(bodyRef), runtime.bodyRefsForSpaceHandle(backendId, spaceHandle)); + assertEquals(List.of(jointRef), runtime.jointRefsForSpaceHandle(backendId, spaceHandle)); + assertSame(backendRuntime, runtime.runtimeForSpaceRef(spaceRef)); + assertSame(backendRuntime, runtime.runtimeForBodyRef(bodyRef)); + assertSame(backendRuntime, runtime.runtimeForJointRef(jointRef)); + assertNull(runtime.runtimeForJointRef(new TestRef(99))); + + runtime.putJointHandle(reboundJointRef, spaceRef, spaceHandle, reboundJointHandle); + runtime.putJointMetadata(backendId, spaceHandle, reboundJointHandle, jointUuid, reboundJointRef); + + assertNull(runtime.getJointHandle(jointRef)); + assertNull(runtime.runtimeForJointRef(jointRef)); + assertEquals(reboundJointHandle, runtime.getJointHandle(reboundJointRef)); + assertSame(backendRuntime, runtime.runtimeForJointRef(reboundJointRef)); + assertEquals(List.of(reboundJointRef), + runtime.jointRefsForSpaceHandle(backendId, spaceHandle)); + + runtime.removeBodyHandle(bodyRef); + runtime.removeJointHandle(reboundJointRef); + + assertEquals(List.of(), runtime.bodyRefsForSpaceHandle(backendId, spaceHandle)); + assertEquals(List.of(), runtime.jointRefsForSpaceHandle(backendId, spaceHandle)); + } + + @Test + void runtimeBodyHandleReplacementRemovesPreviousSpaceIndexEntry() { + PhysicsRuntimeResource runtime = new PhysicsRuntimeResource(); + UUID spaceUuid = UUID.fromString("00000000-0000-0000-0000-000000000022"); + UUID bodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000023"); + BackendId backendId = new BackendId("test:runtime-body-replace"); + BackendSpaceHandle spaceHandle = new BackendSpaceHandle(61); + BackendBodyHandle firstHandle = new BackendBodyHandle(62L); + BackendBodyHandle secondHandle = new BackendBodyHandle(63L); + Ref spaceRef = new TestRef(40); + Ref bodyRef = new TestRef(41); + + runtime.putSpaceHandle(spaceRef, backendId, spaceHandle); + runtime.putSpaceMetadata(backendId, spaceHandle, spaceUuid, spaceRef); + runtime.putBodyHandle(bodyRef, spaceRef, spaceHandle, firstHandle); + runtime.putBodySnapshotMetadata(backendId, + spaceHandle, + firstHandle, + bodyUuid, + bodyRef, + spaceUuid); + runtime.putBodyHitMetadata(backendId, + spaceHandle, + firstHandle, + bodyUuid, + bodyRef, + PhysicsBodyType.DYNAMIC, + ShapeType.BOX); + + runtime.putBodyHandle(bodyRef, spaceRef, spaceHandle, secondHandle); + runtime.putBodySnapshotMetadata(backendId, + spaceHandle, + secondHandle, + bodyUuid, + bodyRef, + spaceUuid); + runtime.putBodyHitMetadata(backendId, + spaceHandle, + secondHandle, + bodyUuid, + bodyRef, + PhysicsBodyType.DYNAMIC, + ShapeType.BOX); + + List handles = new ArrayList<>(); + runtime.forEachBodyHandle(backendId, spaceHandle, handles::add); + assertEquals(List.of(secondHandle.value()), handles); + assertEquals(1, runtime.bodyHandleCount(backendId, spaceHandle)); + assertNull(runtime.getBodySnapshotMetadata(backendId, spaceHandle, firstHandle.value())); + assertNull(runtime.getBodyHitMetadata(backendId, spaceHandle, firstHandle.value())); + assertEquals(bodyUuid, + runtime.getBodySnapshotMetadata(backendId, spaceHandle, secondHandle.value()).bodyUuid()); + } + + @Test + void runtimeBindsLargeDistinctBodyMetadataSetWithoutQuadraticDuplicateScan() { + PhysicsRuntimeResource runtime = new PhysicsRuntimeResource(); + UUID spaceUuid = UUID.fromString("00000000-0000-0000-0000-000000000033"); + BackendId backendId = new BackendId("test:runtime-body-metadata-scale"); + BackendSpaceHandle spaceHandle = new BackendSpaceHandle(71); + Ref spaceRef = new TestRef(70); + runtime.putSpaceHandle(spaceRef, backendId, spaceHandle); + runtime.putSpaceMetadata(backendId, spaceHandle, spaceUuid, spaceRef); + + for (int index = 0; index < 20_000; index++) { + Ref bodyRef = new TestRef(1_000 + index); + BackendBodyHandle bodyHandle = new BackendBodyHandle(10_000L + index); + runtime.putBodyHandle(bodyRef, spaceRef, spaceHandle, bodyHandle); + runtime.putBodySnapshotMetadata(backendId, + spaceHandle, + bodyHandle, + new UUID(0L, 1_000_000L + index), + bodyRef, + spaceUuid); + } + assertEquals(20_000, runtime.bodyHandleCount(backendId, spaceHandle)); + } + + @Test + void runtimeRefreshRebuildsRefIndexesFromScopedBackendMetadata() { + PhysicsRuntimeResource runtime = new PhysicsRuntimeResource(); + PhysicsStore physicsStore = + new PhysicsStore(TestInstanceFactory.world("runtime-refresh-index-test")); + BackendId backendId = new BackendId("test:runtime-refresh"); + PhysicsBackendRuntime backendRuntime = + new FakePhysicsBackendRuntimeProvider(backendId, false, false).createRuntime(); + UUID spaceUuid = UUID.fromString("00000000-0000-0000-0000-000000000019"); + UUID bodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000020"); + UUID jointUuid = UUID.fromString("00000000-0000-0000-0000-000000000021"); + BackendSpaceHandle spaceHandle = new BackendSpaceHandle(55); + BackendBodyHandle bodyHandle = new BackendBodyHandle(56L); + BackendJointHandle jointHandle = new BackendJointHandle(57L); + Ref oldSpaceRef = new TestRef(20); + Ref oldBodyRef = new TestRef(21); + Ref oldJointRef = new TestRef(22); + Ref newSpaceRef = new TestRef(30); + Ref newBodyRef = new TestRef(31); + Ref newJointRef = new TestRef(32); + + runtime.putRuntime(backendId, backendRuntime); + runtime.putSpaceHandle(oldSpaceRef, backendId, spaceHandle); + runtime.putSpaceMetadata(backendId, spaceHandle, spaceUuid, oldSpaceRef); + runtime.putBodyHandle(oldBodyRef, oldSpaceRef, spaceHandle, bodyHandle); + runtime.putBodySnapshotMetadata(backendId, + spaceHandle, + bodyHandle, + bodyUuid, + oldBodyRef, + spaceUuid); + runtime.putJointHandle(oldJointRef, oldSpaceRef, spaceHandle, jointHandle); + runtime.putJointMetadata(backendId, spaceHandle, jointHandle, jointUuid, oldJointRef); + physicsStore.putRefForUUID(spaceUuid, newSpaceRef); + physicsStore.putRefForUUID(bodyUuid, newBodyRef); + physicsStore.putRefForUUID(jointUuid, newJointRef); + + runtime.refreshRowRefs(physicsStore); + + assertNull(runtime.getSpaceHandle(oldSpaceRef)); + assertNull(runtime.getBodyHandle(oldBodyRef)); + assertNull(runtime.getJointHandle(oldJointRef)); + assertEquals(spaceHandle, runtime.getSpaceHandle(newSpaceRef)); + assertEquals(bodyHandle, runtime.getBodyHandle(newBodyRef)); + assertEquals(jointHandle, runtime.getJointHandle(newJointRef)); + assertSame(backendRuntime, runtime.runtimeForSpaceRef(newSpaceRef)); + assertSame(backendRuntime, runtime.runtimeForBodyRef(newBodyRef)); + assertSame(backendRuntime, runtime.runtimeForJointRef(newJointRef)); + assertEquals(newBodyRef, + runtime.getBodySnapshotMetadata(backendId, spaceHandle, bodyHandle.value()).bodyRef()); + } + + @Test + void removeBodyHandleClearsPendingBodyOperationsForThatRef() { + PhysicsRuntimeResource runtime = new PhysicsRuntimeResource(); + UUID firstBodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000011"); + UUID secondBodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000012"); + Ref firstBodyRef = new TestRef(11); + Ref secondBodyRef = new TestRef(12); + + runtime.enqueuePendingBodyOperation(PhysicsRuntimeResource.PendingBodyOperation.wake( + firstBodyUuid, + firstBodyRef)); + runtime.enqueuePendingBodyOperation(PhysicsRuntimeResource.PendingBodyOperation.sleep( + secondBodyUuid, + secondBodyRef)); + + runtime.removeBodyHandle(firstBodyRef); + + List drained = + runtime.drainPendingBodyOperations(); + assertEquals(1, drained.size()); + assertEquals(secondBodyUuid, drained.getFirst().bodyUuid()); + assertEquals(secondBodyRef, drained.getFirst().bodyRef()); + } + + @Test + void snapshotResourceIndexesLatestPublishedFrameByBodyUuid() { + PhysicsSnapshotResource resource = new PhysicsSnapshotResource(); + UUID spaceUuid = UUID.fromString("00000000-0000-0000-0000-000000000005"); + UUID bodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000006"); + PhysicsBodySnapshot body = new PhysicsBodySnapshot(bodyUuid, + spaceUuid, + PhysicsBodyType.KINEMATIC, + new Vector3f(1.0f, 2.0f, 3.0f), + new Quaternionf(), + new Vector3f(4.0f, 5.0f, 6.0f), + new Vector3f(), + 0.25f, + false); + PhysicsSnapshotFrame frame = new PhysicsSnapshotFrame(11L, 0.05f, List.of(body)); + + resource.publish(frame); + + assertNotSame(frame, resource.getLatestFrame()); + assertSnapshotEquals(body, resource.getBody(bodyUuid)); + assertNotSame(body, resource.getBody(bodyUuid)); + assertNull(resource.getBody(UUID.randomUUID())); + + resource.clear(); + + assertEquals(PhysicsSnapshotFrame.EMPTY, resource.getLatestFrame()); + assertNull(resource.getBody(bodyUuid)); + } + + @Test + void snapshotResourceCursorReadsCompactBodyStateWithoutFrameMaterialization() { + PhysicsSnapshotResource resource = new PhysicsSnapshotResource(); + UUID spaceUuid = UUID.fromString("00000000-0000-0000-0000-000000000026"); + UUID firstBodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000027"); + UUID secondBodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000028"); + Ref firstBodyRef = new TestRef(27); + Ref secondBodyRef = new TestRef(28); + PhysicsBodySnapshot first = snapshot(firstBodyRef, firstBodyUuid, spaceUuid); + PhysicsBodySnapshot second = snapshot(secondBodyRef, secondBodyUuid, spaceUuid); + resource.publish(new PhysicsSnapshotFrame(14L, 0.05f, List.of(first, second))); + + List visited = new ArrayList<>(); + resource.forEachBodyCursor(cursor -> { + visited.add(cursor.bodyUuid()); + assertEquals(spaceUuid, cursor.spaceUuid()); + assertEquals(PhysicsBodyType.KINEMATIC, cursor.bodyType()); + assertTrue(cursor.bodyRef() == firstBodyRef || cursor.bodyRef() == secondBodyRef); + assertTrue(cursor.positionX() > 0.0f); + assertEquals(0.0f, cursor.centerOfMassOffsetY(), 0.0001f); + assertFalse(cursor.sleeping()); + }); + + assertEquals(14L, resource.latestSequence()); + assertEquals(List.of(firstBodyUuid, secondBodyUuid), visited); + } + + @Test + void snapshotResourcePublishesCompactPayloadWithoutLegacyFrameMaterialization() { + PhysicsSnapshotResource resource = new PhysicsSnapshotResource(); + UUID spaceUuid = UUID.fromString("00000000-0000-0000-0000-000000000029"); + UUID firstBodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000030"); + UUID secondBodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000031"); + Ref firstBodyRef = new TestRef(30); + Ref secondBodyRef = new TestRef(31); + PhysicsBodySnapshot first = snapshot(firstBodyRef, firstBodyUuid, spaceUuid); + PhysicsBodySnapshot second = snapshot(secondBodyRef, secondBodyUuid, spaceUuid, true); + PhysicsSnapshotResource.CompactSnapshotBuilder builder = + PhysicsSnapshotResource.compactBuilder(2); + addCompactBody(builder, first); + addCompactBody(builder, second); + + resource.publish(15L, 0.06f, builder.build()); + + assertEquals(15L, resource.latestSequence()); + assertEquals(2, resource.bodyCount()); + assertSnapshotEquals(first, resource.getBody(firstBodyUuid)); + assertSnapshotEquals(second, resource.getBody(secondBodyRef)); + List visited = new ArrayList<>(); + resource.forEachBodyCursor(cursor -> visited.add(cursor.bodyUuid())); + assertEquals(List.of(firstBodyUuid, secondBodyUuid), visited); + + PhysicsSnapshotFrame frame = resource.getLatestFrame(); + assertEquals(15L, frame.sequence()); + assertEquals(0.06f, frame.dt()); + assertEquals(2, frame.bodies().size()); + assertSnapshotEquals(first, frame.bodies().getFirst()); + assertSnapshotEquals(second, frame.bodies().get(1)); + } + + @Test + void snapshotResourceRemovesBodiesFromCompactPayloadWithoutLegacyFrameRoundTrip() { + PhysicsSnapshotResource resource = new PhysicsSnapshotResource(); + UUID spaceUuid = UUID.fromString("00000000-0000-0000-0000-000000000032"); + UUID removedBodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000034"); + UUID retainedBodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000035"); + Ref removedBodyRef = new TestRef(34); + Ref retainedBodyRef = new TestRef(35); + PhysicsBodySnapshot removed = snapshot(removedBodyRef, removedBodyUuid, spaceUuid); + PhysicsBodySnapshot retained = snapshot(retainedBodyRef, retainedBodyUuid, spaceUuid); + PhysicsSnapshotResource.CompactSnapshotBuilder builder = + PhysicsSnapshotResource.compactBuilder(2); + addCompactBody(builder, removed); + addCompactBody(builder, retained); + resource.publish(16L, 0.07f, builder.build()); + + resource.removeBody(removedBodyUuid); + + assertEquals(1, resource.bodyCount()); + assertNull(resource.getBody(removedBodyUuid)); + assertNull(resource.getBody(removedBodyRef)); + assertSnapshotEquals(retained, resource.getBody(retainedBodyUuid)); + assertSnapshotEquals(retained, resource.getBody(retainedBodyRef)); + assertEquals(16L, resource.getLatestFrame().sequence()); + assertEquals(0.07f, resource.getLatestFrame().dt()); + assertEquals(List.of(retainedBodyUuid), + resource.getLatestFrame().bodies().stream().map(PhysicsBodySnapshot::bodyUuid).toList()); + } + + @Test + void snapshotResourceRemovesMultipleBodiesInOneBatch() { + PhysicsSnapshotResource resource = new PhysicsSnapshotResource(); + UUID spaceUuid = UUID.fromString("00000000-0000-0000-0000-000000000015"); + UUID firstBodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000016"); + UUID secondBodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000017"); + UUID retainedBodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000018"); + Ref firstBodyRef = new TestRef(16); + Ref secondBodyRef = new TestRef(17); + Ref retainedBodyRef = new TestRef(18); + PhysicsBodySnapshot first = snapshot(firstBodyRef, firstBodyUuid, spaceUuid); + PhysicsBodySnapshot second = snapshot(secondBodyRef, secondBodyUuid, spaceUuid); + PhysicsBodySnapshot retained = snapshot(retainedBodyRef, retainedBodyUuid, spaceUuid); + resource.publish(new PhysicsSnapshotFrame(12L, 0.05f, List.of(first, second, retained))); + + resource.removeBodies(List.of(firstBodyUuid, secondBodyUuid)); + + assertNull(resource.getBody(firstBodyUuid)); + assertNull(resource.getBody(firstBodyRef)); + assertNull(resource.getBody(secondBodyUuid)); + assertNull(resource.getBody(secondBodyRef)); + assertSnapshotEquals(retained, resource.getBody(retainedBodyUuid)); + assertSnapshotEquals(retained, resource.getBody(retainedBodyRef)); + List retainedBodies = resource.getLatestFrame().bodies(); + assertEquals(1, retainedBodies.size()); + assertSnapshotEquals(retained, retainedBodies.getFirst()); + } + + @Test + void chunkCollisionQueueKeepsOnlyLatestMutationPerSourceBeforeDrain() { + PhysicsChunkCollisionMutationQueueResource queue = + new PhysicsChunkCollisionMutationQueueResource(); + UUID spaceUuid = UUID.fromString("00000000-0000-0000-0000-000000000025"); + ChunkCollisionPayload firstPayload = chunkPayload(1.0); + ChunkCollisionPayload secondPayload = chunkPayload(2.0); + + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + "0:1:2", + 0, + 1, + 2, + "chunk-collision/first", + firstPayload)); + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + "3:4:5", + 3, + 4, + 5, + "chunk-collision/other", + firstPayload)); + queue.enqueue(ChunkCollisionMutation.upsert(spaceUuid, + "0:1:2", + 0, + 1, + 2, + "chunk-collision/second", + secondPayload)); + + assertEquals(2, queue.size()); + List drained = queue.drain(); + + assertEquals(2, drained.size()); + assertEquals("3:4:5", drained.get(0).sourceKey()); + assertEquals("0:1:2", drained.get(1).sourceKey()); + assertSame(secondPayload, drained.get(1).payload()); + } + + private static final class TestRef extends Ref { + + private TestRef(int index) { + super(null, index); + } + + @Override + public boolean isValid() { + return true; + } + } + + private static PhysicsBodySnapshot snapshot(Ref bodyRef, + UUID bodyUuid, + UUID spaceUuid) { + return snapshot(bodyRef, bodyUuid, spaceUuid, false); + } + + private static PhysicsBodySnapshot snapshot(Ref bodyRef, + UUID bodyUuid, + UUID spaceUuid, + boolean sleeping) { + return new PhysicsBodySnapshot(bodyRef, + bodyUuid, + spaceUuid, + PhysicsBodyType.KINEMATIC, + new Vector3f(1.0f, 2.0f, 3.0f), + new Quaternionf(), + new Vector3f(), + new Vector3f(), + 0.0f, + sleeping); + } + + private static void addCompactBody(PhysicsSnapshotResource.CompactSnapshotBuilder builder, + PhysicsBodySnapshot body) { + builder.addBody(body.bodyRef(), + body.bodyUuid(), + body.spaceUuid(), + body.bodyType(), + body.positionX(), + body.positionY(), + body.positionZ(), + body.rotationX(), + body.rotationY(), + body.rotationZ(), + body.rotationW(), + body.linearVelocityX(), + body.linearVelocityY(), + body.linearVelocityZ(), + body.angularVelocityX(), + body.angularVelocityY(), + body.angularVelocityZ(), + body.centerOfMassOffsetY(), + body.sleeping()); + } + + private static ChunkCollisionPayload chunkPayload(double centerX) { + return new ChunkCollisionPayload(1.0f, + 1.0f, + 1.0f, + new int[0], + List.of(new ChunkCollisionPayload.BoxPayload(centerX, + 0.0, + 0.0, + 0.5, + 0.5, + 0.5)), + List.of(), + false, + List.of()); + } + + private static void assertSnapshotEquals(PhysicsBodySnapshot expected, + PhysicsBodySnapshot actual) { + assertEquals(expected.bodyRef(), actual.bodyRef()); + assertEquals(expected.bodyUuid(), actual.bodyUuid()); + assertEquals(expected.spaceUuid(), actual.spaceUuid()); + assertEquals(expected.bodyType(), actual.bodyType()); + assertEquals(expected.positionX(), actual.positionX()); + assertEquals(expected.positionY(), actual.positionY()); + assertEquals(expected.positionZ(), actual.positionZ()); + assertEquals(expected.rotationX(), actual.rotationX()); + assertEquals(expected.rotationY(), actual.rotationY()); + assertEquals(expected.rotationZ(), actual.rotationZ()); + assertEquals(expected.rotationW(), actual.rotationW()); + assertEquals(expected.linearVelocityX(), actual.linearVelocityX()); + assertEquals(expected.linearVelocityY(), actual.linearVelocityY()); + assertEquals(expected.linearVelocityZ(), actual.linearVelocityZ()); + assertEquals(expected.angularVelocityX(), actual.angularVelocityX()); + assertEquals(expected.angularVelocityY(), actual.angularVelocityY()); + assertEquals(expected.angularVelocityZ(), actual.angularVelocityZ()); + assertEquals(expected.centerOfMassOffsetY(), actual.centerOfMassOffsetY()); + assertEquals(expected.sleeping(), actual.sleeping()); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldResourceStateTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldResourceStateTest.java deleted file mode 100644 index 2c9ea611..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldResourceStateTest.java +++ /dev/null @@ -1,1535 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources; - -import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsJoint; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend.InMemoryPhysicsSpace; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.FakePhysicsBackendRuntime; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRuntimeState.BodySyncState; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.PhysicsChunkBoundaryRuntime.ChunkBoundaryPauseState; -import dev.hytalemodding.impulse.core.internal.resources.owner.TestPhysicsOwnerLane; -import dev.hytalemodding.impulse.core.internal.simulation.recorder.MutablePhysicsCommandContext; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerSnapshot; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerStepCommand; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionMode; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsCommandBatchEvent; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsSnapshotPublicationEvent; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsStepEvent; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandResult; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RigidBodyStateQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.view.RigidBodyStateView; -import dev.hytalemodding.impulse.core.plugin.simulation.query.SpaceBodyCountQuery; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import javax.annotation.Nonnull; -import org.joml.Quaternionf; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class PhysicsWorldResourceStateTest { - - private static final AtomicInteger BACKEND_COUNTER = new AtomicInteger(); - - @Test - void worldSettingsAreCopiedAcrossResourceBoundary() { - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - - PhysicsWorldSettings copy = resource.getWorldSettings(); - copy.setSimulationSteps(4); - - assertEquals(PhysicsWorldSettings.MIN_SIMULATION_STEPS, - resource.getWorldSettings().getSimulationSteps()); - - resource.setWorldSettings(copy); - assertEquals(4, resource.getWorldSettings().getSimulationSteps()); - - copy.setSimulationSteps(2); - assertEquals(4, resource.getWorldSettings().getSimulationSteps()); - } - - @Test - void spaceSettingsAreCopiedAcrossResourceBoundary() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:space-settings-copy-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpaceSettings initial = PhysicsSpaceSettings.streamingWorldCollision(); - initial.getSolverSettings().setSolverIterations(7); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - initial); - - PhysicsSpaceSettings copy = resource.getSpaceSettings(space.id()); - copy.getSolverSettings().setSolverIterations(3); - copy.getWorldCollisionSettings().setWorldCollisionMode(WorldCollisionMode.NONE); - - assertEquals(7, resource.getSpaceSettings(space.id()).getSolverSettings().getSolverIterations()); - assertEquals(WorldCollisionMode.STREAMING, - resource.getSpaceSettings(space.id()).getWorldCollisionSettings().getWorldCollisionMode()); - assertEquals(7, ((InMemoryPhysicsSpace) space).getSolverIterations()); - - resource.setSpaceSettings(space.id(), copy); - assertEquals(3, resource.getSpaceSettings(space.id()).getSolverSettings().getSolverIterations()); - assertEquals(WorldCollisionMode.NONE, - resource.getSpaceSettings(space.id()).getWorldCollisionSettings().getWorldCollisionMode()); - assertEquals(3, ((InMemoryPhysicsSpace) space).getSolverIterations()); - - copy.getSolverSettings().setSolverIterations(5); - assertEquals(3, resource.getSpaceSettings(space.id()).getSolverSettings().getSolverIterations()); - } - - @Test - void copyFromCopiesWorldSettingsWithoutLiveSpaceMetadata() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:copy-topology-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource source = new LegacyLiveHandleTestResource(); - PhysicsWorldSettings sourceSettings = source.getWorldSettings(); - sourceSettings.setSimulationSteps(4); - source.setWorldSettings(sourceSettings); - PhysicsSpaceSettings sourceSpaceSettings = PhysicsSpaceSettings.streamingWorldCollision(); - PhysicsSpace sourceSpace = source.createLiveSpace(backend.getId(), - "source-world", - sourceSpaceSettings); - - LegacyLiveHandleTestResource target = new LegacyLiveHandleTestResource(); - PhysicsSpace targetSpace = target.createLiveSpace(backend.getId(), - "target-world", - PhysicsSpaceSettings.defaults()); - - target.copyFrom(source); - - assertEquals(4, target.getWorldSettings().getSimulationSteps()); - assertEquals(0, target.getSpaceCount()); - assertTrue(target.getSpaceIds().isEmpty()); - assertFalse(target.hasSpace(sourceSpace.id())); - assertEquals(0L, target.worldCollisionStreamingRevision(targetSpace.id())); - assertThrows(IllegalStateException.class, () -> target.getSpaceSettings(sourceSpace.id())); - assertTrue(((InMemoryPhysicsSpace) targetSpace).isClosed()); - assertFalse(((InMemoryPhysicsSpace) sourceSpace).isClosed()); - } - - @Test - void bodySyncStateTracksLastSyncAndClampsNegativeSkipTime() { - BodySyncState syncState = new BodySyncState(); - - syncState.recordSync(new Vector3f(1.0f, 2.0f, 3.0f), - new Quaternionf().rotateY(0.5f), - true); - syncState.recordSkip(-5.0f); - syncState.recordSkip(0.75f); - - assertTrue(syncState.isInitialized()); - assertTrue(syncState.isSleeping()); - assertEquals(0.75f, syncState.getSecondsSinceSync(), 0.0001f); - assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), syncState.getLastSyncedPosition()); - assertEquals(new Quaternionf().rotateY(0.5f), syncState.getLastSyncedRotation()); - } - - @Test - void chunkBoundaryPauseStateCopiesProvidedVectors() { - ChunkBoundaryPauseState state = - new ChunkBoundaryPauseState(); - Vector3f linear = new Vector3f(1.0f, 2.0f, 3.0f); - Vector3f angular = new Vector3f(4.0f, 5.0f, 6.0f); - - state.set(42L, PhysicsBodyType.KINEMATIC, linear, angular); - linear.zero(); - angular.zero(); - - assertEquals(42L, state.getTargetChunkIndex()); - assertArrayEquals(new long[] {42L}, state.getTargetChunkIndices()); - assertEquals(PhysicsBodyType.KINEMATIC, state.getOriginalBodyType()); - assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), state.getLinearVelocity()); - assertEquals(new Vector3f(4.0f, 5.0f, 6.0f), state.getAngularVelocity()); - } - - @Test - void chunkBoundaryPauseStateCopiesTargetChunkFootprint() { - ChunkBoundaryPauseState state = new ChunkBoundaryPauseState(); - long[] targetChunks = {10L, 11L}; - - state.set(10L, targetChunks, physicsSnapshot(PhysicsBodyType.DYNAMIC)); - targetChunks[0] = 99L; - - assertEquals(10L, state.getTargetChunkIndex()); - assertArrayEquals(new long[] {10L, 11L}, state.getTargetChunkIndices()); - } - - @Test - void simulationCommandBufferMutatesBodiesOnOwnerLane() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:simulation-command-buffer-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - RigidBodyKey bodyKey = RigidBodyKey.random(); - var handle = resource.submitCommands(99L, 2, commands -> commands - .spawnBody(bodyKey, spawn -> spawn - .space(space.id()) - .box(0.5f, 0.5f, 0.5f) - .mass(1.0f) - .dynamic() - .position(1.0f, 2.0f, 3.0f) - .kind(PhysicsBodyKind.BODY) - .persistence(PhysicsBodyPersistenceMode.RUNTIME_ONLY)) - .body(bodyKey) - .setVelocity(4.0f, 5.0f, 6.0f, 0.1f, 0.2f, 0.3f, true)); - var results = handle.completion().toCompletableFuture().join(); - - assertTrue(handle.completionSummary().toCompletableFuture().join().allApplied()); - assertEquals(2, results.size()); - assertEquals(1L, results.getFirst().commandSequence()); - assertEquals(2L, results.get(1).commandSequence()); - PhysicsBody body = resource.getBody(bodyKey); - assertNotNull(body); - assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), body.getPosition()); - assertEquals(new Vector3f(4.0f, 5.0f, 6.0f), body.getLinearVelocity()); - assertEquals(new Vector3f(0.1f, 0.2f, 0.3f), body.getAngularVelocity()); - } - - @Test - void staleCommandBatchRejectsWithoutMutatingAfterWorldEpochChange() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:simulation-stale-command-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - RigidBodyKey bodyKey = RigidBodyKey.random(); - MutablePhysicsCommandContext commands = resource.createMutableCommandContext(107L); - commands.spawnBody(bodyKey, spawn -> spawn - .space(space.id()) - .box(0.5f, 0.5f, 0.5f) - .dynamic()); - - resource.resetRuntimeStateKeepingSpaces("test-world"); - - var results = resource.submitRecordedCommands(commands) - .completion() - .toCompletableFuture() - .join(); - - assertEquals(1, results.size()); - assertEquals(PhysicsCommandResult.Status.REJECTED, results.getFirst().status()); - assertTrue(results.getFirst().commandBatchSequence() > 0L); - assertEquals(107L, results.getFirst().submittedServerTick()); - assertEquals(0L, results.getFirst().includedSnapshotFrameEpoch()); - assertTrue(results.getFirst().message().contains("stale")); - assertNull(resource.getBody(bodyKey)); - assertEquals(0, resource.query(new SpaceBodyCountQuery(space.id())) - .completion() - .toCompletableFuture() - .join()); - } - - @Test - void sameEpochCommandBuffersExecuteInFifoOrderAfterTopologyMutation() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:simulation-fifo-command-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - RigidBodyKey firstKey = RigidBodyKey.random(); - RigidBodyKey secondKey = RigidBodyKey.random(); - MutablePhysicsCommandContext first = resource.createMutableCommandContext(108L); - first.spawnBody(firstKey, spawn -> spawn - .space(space.id()) - .box(0.5f, 0.5f, 0.5f) - .dynamic()); - MutablePhysicsCommandContext second = resource.createMutableCommandContext(108L); - second.spawnBody(secondKey, spawn -> spawn - .space(space.id()) - .box(0.5f, 0.5f, 0.5f) - .dynamic()); - - var firstResults = resource.submitRecordedCommands(first) - .completion() - .toCompletableFuture() - .join(); - var secondResults = resource.submitRecordedCommands(second) - .completion() - .toCompletableFuture() - .join(); - - assertEquals(PhysicsCommandResult.Status.APPLIED, firstResults.getFirst().status()); - assertEquals(PhysicsCommandResult.Status.APPLIED, secondResults.getFirst().status()); - assertNotNull(resource.getBody(firstKey)); - assertNotNull(resource.getBody(secondKey)); - assertEquals(2, resource.query(new SpaceBodyCountQuery(space.id())) - .completion() - .toCompletableFuture() - .join()); - } - - @Test - void simulationCommandCanSetSpaceGravity() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:simulation-space-gravity-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - - var results = resource.submitCommands(104L, - commands -> commands.setSpaceGravity(space.id(), 0.0f, -9.81f, 0.0f)) - .completion() - .toCompletableFuture() - .join(); - - assertEquals(1, results.size()); - assertEquals(PhysicsCommandResult.Status.APPLIED, results.getFirst().status()); - assertEquals(new Vector3f(0.0f, -9.81f, 0.0f), space.getGravity()); - } - - @Test - void simulationDslRecipesSubmitCopiedCommands() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:simulation-dsl-submit-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - RigidBodyKey bodyKey = RigidBodyKey.random(); - - var results = resource.submitCommands(104L, commands -> commands - .spawnBody(bodyKey, spawn -> spawn - .space(space.id()) - .box(0.5f, 0.5f, 0.5f) - .position(1.0f, 2.0f, 3.0f) - .dynamic()) - .body(bodyKey) - .setVelocity(new Vector3f(2.0f, 0.0f, 0.0f), new Vector3f(), true)) - .completion() - .toCompletableFuture() - .join(); - - assertEquals(2, results.size()); - PhysicsBody body = resource.getBody(bodyKey); - assertNotNull(body); - assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), body.getPosition()); - assertEquals(new Vector3f(2.0f, 0.0f, 0.0f), body.getLinearVelocity()); - } - - @Test - void commandCompletionExposesOwnerExecutionMetadataBeforeSnapshotPublication() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:simulation-command-latency-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - RigidBodyKey bodyKey = RigidBodyKey.random(); - - var results = resource.submitCommands(123L, commands -> commands - .spawnBody(bodyKey, spawn -> spawn - .space(space.id()) - .box(0.5f, 0.5f, 0.5f) - .dynamic())) - .completion() - .toCompletableFuture() - .join(); - - PhysicsCommandResult result = results.getFirst(); - assertEquals(PhysicsCommandResult.Status.APPLIED, result.status()); - assertEquals(1L, result.commandSequence()); - assertTrue(result.commandBatchSequence() > 0L); - assertEquals(123L, result.submittedServerTick()); - assertEquals(0L, result.includedSnapshotFrameEpoch()); - PublishedPhysicsSnapshotFrame frameAtCompletion = resource.getLatestPublishedFrame(); - assertEquals(PublishedPhysicsSnapshotFrame.Status.EMPTY, frameAtCompletion.status()); - assertEquals(0, frameAtCompletion.bodyCount()); - - PublishedPhysicsSnapshotFrame published = resource.capturePublishedSnapshotFrame(77L, - 124L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - - assertTrue(published.frameEpoch() > result.includedSnapshotFrameEpoch()); - assertEquals(1, published.bodyCount()); - assertEquals(124L, published.serverTick()); - } - - @Test - void commandCompletionPublishesValueOnlyEventFrameBeforeSnapshotVisibility() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:simulation-command-event-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - RigidBodyKey bodyKey = RigidBodyKey.random(); - - var handle = resource.submitCommands(127L, commands -> commands - .spawnBody(bodyKey, spawn -> spawn - .space(space.id()) - .box(0.5f, 0.5f, 0.5f) - .dynamic())); - var results = handle.completion() - .toCompletableFuture() - .join(); - - PhysicsCommandResult result = results.getFirst(); - PhysicsEventFrame eventFrame = resource.getLatestEventFrame(); - PhysicsCommandBatchEvent event = eventFrame.commandBatches().getFirst(); - - assertEquals(1, eventFrame.commandBatchCount()); - assertEquals(resource.worldEpoch(), eventFrame.worldEpoch()); - assertEquals(0L, eventFrame.latestCapturedSnapshotFrameEpoch()); - assertEquals(0L, eventFrame.latestCapturedSnapshotLastIncludedCommandBatchSequence()); - assertFalse(handle.isIncludedInLatestCapturedSnapshot(eventFrame)); - assertEquals(0L, handle.capturedSnapshotServerTickLatency(eventFrame)); - assertEquals(result.commandBatchSequence(), event.commandBatchSequence()); - assertEquals(127L, event.submittedServerTick()); - assertEquals(1, event.commandCount()); - assertTrue(event.allApplied()); - assertEquals(0L, event.firstRejectedCommandSequence()); - assertNull(event.firstRejectedMessage()); - - PublishedPhysicsSnapshotFrame published = resource.capturePublishedSnapshotFrame(79L, - 128L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - - assertTrue(published.lastIncludedCommandBatchSequence() >= event.commandBatchSequence()); - assertTrue(handle.isIncludedInSnapshotFrame(published)); - assertEquals(1L, handle.capturedSnapshotServerTickLatency(published)); - PhysicsEventFrame capturedEventFrame = resource.getLatestEventFrame(); - assertTrue(handle.isIncludedInLatestCapturedSnapshot(capturedEventFrame)); - assertEquals(1L, handle.capturedSnapshotServerTickLatency(capturedEventFrame)); - assertEquals(0L, result.includedSnapshotFrameEpoch()); - } - - @Test - void snapshotCaptureAndPublicationPublishValueOnlyEventFrames() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:simulation-snapshot-event-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - RigidBodyKey bodyKey = RigidBodyKey.random(); - resource.submitCommands(129L, commands -> commands - .spawnBody(bodyKey, spawn -> spawn - .space(space.id()) - .box(0.5f, 0.5f, 0.5f) - .dynamic())) - .completion() - .toCompletableFuture() - .join(); - - PublishedPhysicsSnapshotFrame captured = resource.capturePublishedSnapshotFrame(80L, - 130L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 12L, - false); - PhysicsEventFrame stepFrame = resource.getLatestEventFrame(); - PhysicsStepEvent stepEvent = stepFrame.steps().getFirst(); - - assertEquals(1, stepFrame.stepCount()); - assertEquals(0, stepFrame.commandBatchCount()); - assertEquals(captured.frameEpoch(), stepFrame.latestCapturedSnapshotFrameEpoch()); - assertEquals(captured.lastIncludedCommandBatchSequence(), - stepFrame.latestCapturedSnapshotLastIncludedCommandBatchSequence()); - assertEquals(80L, stepEvent.stepSequence()); - assertEquals(130L, stepEvent.serverTick()); - assertEquals(captured.frameEpoch(), stepEvent.snapshotFrameEpoch()); - assertEquals(captured.status(), stepEvent.snapshotStatus()); - assertEquals(captured.lastIncludedCommandBatchSequence(), stepEvent.lastIncludedCommandBatchSequence()); - assertEquals(1, stepEvent.bodyCount()); - - int applied = resource.applyPublishedSnapshotFrame(captured); - PhysicsEventFrame publicationFrame = resource.getLatestEventFrame(); - PhysicsSnapshotPublicationEvent publicationEvent = publicationFrame.snapshotPublications().getFirst(); - - assertEquals(1, applied); - assertEquals(1, publicationFrame.snapshotPublicationCount()); - assertEquals(0, publicationFrame.stepCount()); - assertEquals(captured.frameEpoch(), publicationEvent.snapshotFrameEpoch()); - assertEquals(captured.stepSequence(), publicationEvent.stepSequence()); - assertEquals(captured.serverTick(), publicationEvent.serverTick()); - assertEquals(captured.lastIncludedCommandBatchSequence(), - publicationEvent.lastIncludedCommandBatchSequence()); - assertEquals(applied, publicationEvent.appliedBodyCount()); - } - - @Test - void eventFrameDistinguishesOlderCapturedSnapshotFromLaterCommandCompletion() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:simulation-event-latency-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - - PublishedPhysicsSnapshotFrame capturedBeforeCommand = resource.capturePublishedSnapshotFrame(81L, - 131L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - long capturedLastIncludedCommandBatchSequence = capturedBeforeCommand.lastIncludedCommandBatchSequence(); - - var handle = resource.submitCommands(132L, commands -> commands - .setSpaceGravity(space.id(), 0.0f, -4.0f, 0.0f)); - var results = handle.completion() - .toCompletableFuture() - .join(); - PhysicsCommandResult commandResult = results.getFirst(); - PhysicsEventFrame commandEventFrame = resource.getLatestEventFrame(); - PhysicsCommandBatchEvent commandEvent = commandEventFrame.commandBatches().getFirst(); - - assertEquals(commandResult.commandBatchSequence(), commandEvent.commandBatchSequence()); - assertTrue(commandEvent.commandBatchSequence() > capturedLastIncludedCommandBatchSequence); - assertFalse(commandEventFrame.latestCapturedSnapshotIncludesCommandBatch( - commandEvent.commandBatchSequence())); - assertFalse(commandEventFrame.latestCapturedSnapshotIncludes(commandEvent)); - assertFalse(handle.isIncludedInLatestCapturedSnapshot(commandEventFrame)); - - int applied = resource.applyPublishedSnapshotFrame(capturedBeforeCommand); - PhysicsEventFrame publicationEventFrame = resource.getLatestEventFrame(); - PhysicsSnapshotPublicationEvent publicationEvent = - publicationEventFrame.snapshotPublications().getFirst(); - - assertEquals(0, applied); - assertEquals(capturedBeforeCommand.frameEpoch(), publicationEvent.snapshotFrameEpoch()); - assertEquals(capturedLastIncludedCommandBatchSequence, publicationEvent.lastIncludedCommandBatchSequence()); - assertFalse(publicationEventFrame.latestCapturedSnapshotIncludesCommandBatch( - commandEvent.commandBatchSequence())); - assertFalse(handle.isIncludedInLatestCapturedSnapshot(publicationEventFrame)); - } - - @Test - void publishedSnapshotCaptureKeepsBodiesInCompactStorageUntilPublicListsAreRequested() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:compact-published-frame-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - RigidBodyKey bodyKey = RigidBodyKey.random(); - resource.submitCommands(125L, commands -> commands - .spawnBody(bodyKey, spawn -> spawn - .space(space.id()) - .box(0.5f, 0.5f, 0.5f) - .dynamic())) - .completion() - .toCompletableFuture() - .join(); - - PublishedPhysicsSnapshotFrame published = resource.capturePublishedSnapshotFrame(78L, - 126L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - List visitedBodies = new ArrayList<>(); - - published.forEachBodyCursor(body -> visitedBodies.add(body.bodyKey())); - - assertEquals(1, published.bodyCount()); - assertEquals(List.of(bodyKey), visitedBodies); - assertNotNull(published.spaces()); - } - - @Test - void simulationQueryReturnsCopiedOwnerDataWithoutLiveHandles() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:simulation-query-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - RigidBodyKey bodyKey = RigidBodyKey.random(); - PhysicsBody body = space.createSphere(0.5f, 1.0f); - body.setPosition(0.0f, 0.0f, 0.0f); - resource.addBody(bodyKey, - space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - - int bodyCount = resource.query(new SpaceBodyCountQuery(space.id())) - .completion() - .toCompletableFuture() - .join(); - - assertEquals(1, bodyCount); - } - - @Test - void duplicateBodyKeyDoesNotLeaveUnregisteredBackendBodyInSpace() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:duplicate-body-key-no-leak-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - RigidBodyKey bodyKey = RigidBodyKey.random(); - PhysicsBody first = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody second = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - - resource.addBody(bodyKey, - space.id(), - first, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - - assertThrows(IllegalArgumentException.class, () -> resource.addBody(bodyKey, - space.id(), - second, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY)); - - assertEquals(1, space.bodyCount()); - assertFalse(space.getBodies().contains(second)); - assertSame(first, resource.getBody(bodyKey)); - } - - @Test - void simulationSpawnSettingsAndStateQueryUseCopiedData() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:simulation-spawn-settings-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - RigidBodyKey bodyKey = RigidBodyKey.random(); - resource.submitCommands(101L, commands -> commands - .spawnBody(bodyKey, spawn -> spawn - .space(space.id()) - .sphere(0.25f) - .mass(1.0f) - .type(PhysicsBodyType.KINEMATIC) - .position(7.0f, 8.0f, 9.0f) - .settings(RigidBodySpawnSettings.of(0.35f, 0.2f, 0.03f, 0.4f, 7, 11, true)) - .kind(PhysicsBodyKind.TEMPORARY) - .persistence(PhysicsBodyPersistenceMode.RUNTIME_ONLY))) - .completion() - .toCompletableFuture() - .join(); - - PhysicsBody body = resource.getBody(bodyKey); - assertNotNull(body); - assertEquals(PhysicsBodyType.KINEMATIC, body.getBodyType()); - assertEquals(new Vector3f(7.0f, 8.0f, 9.0f), body.getPosition()); - assertEquals(0.35f, body.getFriction(), 0.0001f); - assertEquals(0.2f, body.getRestitution(), 0.0001f); - assertEquals(0.03f, body.getLinearDamping(), 0.0001f); - assertEquals(0.4f, body.getAngularDamping(), 0.0001f); - assertEquals(7, body.getCollisionGroup()); - assertEquals(11, body.getCollisionMask()); - assertTrue(body.isSensor()); - - RigidBodyStateView state = resource.query(new RigidBodyStateQuery(bodyKey)) - .completion() - .toCompletableFuture() - .join() - .orElseThrow(); - assertEquals(bodyKey, state.bodyKey()); - assertEquals(PhysicsBodyType.KINEMATIC, state.bodyType()); - assertEquals(new Vector3f(7.0f, 8.0f, 9.0f), state.pose().position()); - } - - @Test - void simulationTemplatedBulkSpawnAddsBodiesWithSharedCopiedSettings() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:simulation-template-spawn-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - RigidBodyKey firstKey = RigidBodyKey.random(); - RigidBodyKey secondKey = RigidBodyKey.random(); - resource.submitCommands(101L, commands -> commands.spawnBodies(2, - space.id(), - PhysicsShapeSpec.box(0.4f, 0.4f, 0.4f), - 1.0f, - PhysicsBodyType.DYNAMIC, - RigidBodySpawnSettings.material(0.25f, 0.1f), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY, - spawns -> spawns - .body(firstKey, 1.0f, 2.0f, 3.0f) - .body(secondKey, 4.0f, 5.0f, 6.0f))) - .completion() - .toCompletableFuture() - .join(); - - assertEquals(2, space.bodyCount()); - PhysicsBody first = resource.getBody(firstKey); - PhysicsBody second = resource.getBody(secondKey); - assertNotNull(first); - assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), first.getPosition()); - assertNotNull(second); - assertEquals(new Vector3f(4.0f, 5.0f, 6.0f), second.getPosition()); - assertEquals(0.25f, first.getFriction(), 0.0001f); - assertEquals(0.1f, second.getRestitution(), 0.0001f); - } - - @Test - void simulationCommandCanDestroyJointBetweenBodiesWithoutStoredJointKey() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:simulation-destroy-joint-between-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - RigidBodyKey bodyAKey = RigidBodyKey.random(); - RigidBodyKey bodyBKey = RigidBodyKey.random(); - PhysicsBody bodyA = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody bodyB = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - resource.addBody(bodyAKey, - space.id(), - bodyA, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - resource.addBody(bodyBKey, - space.id(), - bodyB, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - PhysicsJoint pointJoint = space.createPointJoint(bodyA, bodyB, new Vector3f(), new Vector3f()); - resource.addJoint(space.id(), pointJoint); - - resource.submitCommands(102L, - commands -> commands.destroyJointBetween(null, space.id(), bodyAKey, bodyBKey)) - .completion() - .toCompletableFuture() - .join(); - - assertEquals(0, space.jointCount()); - - JointKey jointKey = JointKey.random(); - resource.submitCommands(103L, 2, commands -> commands - .joint(jointKey, joint -> joint - .space(space.id()) - .bodies(bodyAKey, bodyBKey) - .point(new Vector3f(), new Vector3f())) - .destroyJointBetween(jointKey, space.id(), bodyAKey, bodyBKey)) - .completion() - .toCompletableFuture() - .join(); - - assertEquals(0, space.jointCount()); - } - - @Test - void resetRuntimeStateKeepingSpacesReplacesNativeSpacesAndClearsRuntimeState() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:reset-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpaceSettings settings = PhysicsSpaceSettings.streamingWorldCollision(); - settings.getSolverSettings().setSolverIterations(7); - settings.getVisualMaterializationSettings().setDetachedVisualMaxMaterialized(64); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), "test-world", settings); - space.setGravity(0.0f, -3.0f, 0.0f); - - PhysicsBody first = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody second = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - RigidBodyKey firstId = resource.addBody(RigidBodyKey.random(), - space.id(), - first, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - resource.addBody(space.id(), - second, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - PhysicsJoint fixedJoint = space.createFixedJoint(first, second, new Vector3f(), new Vector3f()); - resource.addJoint(space.id(), fixedJoint); - resource.markContinuousCollisionForced(firstId); - resource.markBodyControlled(firstId); - resource.updateChunkBoundarySafeState(firstId, new Vector3f(1.0f), new Quaternionf()); - - PhysicsRuntimeResetResult reset = - resource.resetRuntimeStateKeepingSpaces("test-world"); - - InMemoryPhysicsSpace original = backend.createdSpaces().get(0); - InMemoryPhysicsSpace replacement = backend.createdSpaces().get(1); - assertEquals(2, reset.removedBodies()); - assertEquals(1, reset.removedJoints()); - assertEquals(1, reset.keptSpaces()); - assertTrue(original.isClosed()); - assertFalse(replacement.isClosed()); - PhysicsSpace restoredSpace = resource.callOwner("resolve reset replacement space", - () -> resource.getLiveSpace(space.id())); - assertSame(replacement, restoredSpace); - assertNotSame(original, restoredSpace); - assertEquals(new Vector3f(0.0f, -3.0f, 0.0f), replacement.getGravity()); - assertEquals(0, replacement.bodyCount()); - assertEquals(0, replacement.jointCount()); - assertEquals(7, replacement.getSolverIterations()); - - PhysicsSpaceSettings preserved = resource.getSpaceSettings(space.id()); - assertEquals(WorldCollisionMode.STREAMING, preserved.getWorldCollisionSettings().getWorldCollisionMode()); - assertEquals(64, preserved.getVisualMaterializationSettings().getDetachedVisualMaxMaterialized()); - assertEquals(0, resource.getBodyRegistrationViews().size()); - assertEquals(0, resource.getBodySnapshotCount()); - assertNull(resource.getBody(firstId)); - assertFalse(resource.isBodyControlled(firstId)); - assertNull(resource.getChunkBoundarySafeState(firstId)); - assertForcedCcdRestoreDoesNotAffectReusedBodyId(resource, replacement, firstId); - } - - @Test - void failedRuntimeResetKeepsOriginalSpacesAndDiscardsReplacements() { - BackendId backendId = - new BackendId("test:reset-failure-" + BACKEND_COUNTER.incrementAndGet()); - FakePhysicsBackendRuntimeProvider provider = - new FakePhysicsBackendRuntimeProvider(backendId, false, false).withSolverTuning(); - Impulse.registerRuntimeProvider(provider); - - PhysicsWorldRuntimeResource resource = new PhysicsWorldRuntimeResource(); - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - settings.getSolverSettings().setSolverIterations(7); - SpaceId spaceId = resource.createSpace(backendId, "test-world", settings); - PhysicsSpaceBinding original = resource.requireSpaceBinding(spaceId); - FakePhysicsBackendRuntime originalRuntime = - (FakePhysicsBackendRuntime) original.runtime(); - - provider.failNextSolverTuning(new IllegalStateException("replacement tuning failed")); - - IllegalStateException failure = assertThrows(IllegalStateException.class, - () -> resource.resetRuntimeStateKeepingSpaces("test-world")); - - assertEquals("replacement tuning failed", failure.getMessage()); - assertSame(original, resource.requireSpaceBinding(spaceId)); - assertTrue(originalRuntime.hasSpace(original.backendSpaceHandle().value())); - assertEquals(2, provider.createdRuntimes().size()); - FakePhysicsBackendRuntime failedReplacement = provider.createdRuntimes().get(1); - assertFalse(failedReplacement.hasSpace(spaceId.value())); - } - - @Test - void removeSpaceClosesBackendSpaceWhenRuntimeCleanupThrows() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:remove-space-close-failure-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - ThrowingSyncCleanupResource resource = new ThrowingSyncCleanupResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - RigidBodyKey bodyId = resource.addBody(space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - resource.registerBodyAttachment(bodyId, new TestRef(true)); - - IllegalStateException failure = assertThrows(IllegalStateException.class, - () -> resource.removeSpace(space.id(), "test-world")); - - assertEquals("sync cleanup failed", failure.getMessage()); - assertTrue(((InMemoryPhysicsSpace) space).isClosed()); - } - - @Test - void worldCollisionStreamingRevisionTracksSettingsChangesAndClears() { - BackendId backendId = - new BackendId("test:world-collision-revision-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerRuntimeProvider(new FakePhysicsBackendRuntimeProvider(backendId, false, false)); - - PhysicsWorldRuntimeResource resource = new PhysicsWorldRuntimeResource(); - SpaceId spaceId = resource.createSpace(backendId, - "test-world", - PhysicsSpaceSettings.streamingWorldCollision()); - long initialRevision = resource.worldCollisionStreamingRevision(spaceId); - - PhysicsSpaceSettings visualOnly = resource.getSpaceSettings(spaceId); - visualOnly.getVisualMaterializationSettings().setDetachedVisualMaxMaterialized(32); - resource.setSpaceSettings(spaceId, visualOnly); - assertEquals(initialRevision, resource.worldCollisionStreamingRevision(spaceId)); - - PhysicsSpaceSettings radiusChanged = resource.getSpaceSettings(spaceId); - radiusChanged.getWorldCollisionSettings().setWorldCollisionRadius( - radiusChanged.getWorldCollisionSettings().getWorldCollisionRadius() + 1); - resource.setSpaceSettings(spaceId, radiusChanged); - long radiusRevision = resource.worldCollisionStreamingRevision(spaceId); - assertTrue(radiusRevision > initialRevision); - - PhysicsSpaceSettings materialChanged = resource.getSpaceSettings(spaceId); - materialChanged.getWorldCollisionSettings().setTerrainMaterial(0.8f, 0.1f); - resource.setSpaceSettings(spaceId, materialChanged); - long materialRevision = resource.worldCollisionStreamingRevision(spaceId); - assertTrue(materialRevision > radiusRevision); - - resource.clearWorldCollision(spaceId); - long clearRevision = resource.worldCollisionStreamingRevision(spaceId); - assertTrue(clearRevision > materialRevision); - } - - @Test - void destroyBodyClearsRuntimeStateAndSnapshotIndexes() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:destroy-cleanup-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - body.setContinuousCollisionEnabled(true); - RigidBodyKey bodyId = resource.addBody(space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - resource.markContinuousCollisionForced(bodyId); - resource.markBodyControlled(bodyId); - resource.updateChunkBoundarySafeState(bodyId, new Vector3f(1.0f), new Quaternionf()); - resource.pauseChunkBoundaryBody(bodyId, - 42L, - PhysicsBodyType.DYNAMIC, - new Vector3f(2.0f, 0.0f, 0.0f), - new Vector3f(0.0f, 3.0f, 0.0f)); - resource.getBodySnapshot(bodyId); - - resource.destroyBody(bodyId); - - assertEquals(0, space.bodyCount()); - assertNull(resource.getBody(bodyId)); - assertFalse(resource.isBodyControlled(bodyId)); - assertNull(resource.getChunkBoundarySafeState(bodyId)); - assertNull(resource.getChunkBoundaryPauseState(bodyId)); - assertEquals(0, resource.getBodySnapshotCount()); - assertEquals(0, resource.getBodySnapshotCount(space.id())); - assertEquals(0, resource.getBodySnapshotCellCount()); - assertNull(resource.getBodySnapshotIfRegistered(bodyId)); - assertThrows(IllegalArgumentException.class, () -> resource.getBodySnapshot(bodyId)); - assertForcedCcdRestoreDoesNotAffectReusedBodyId(resource, space, bodyId); - } - - @Test - void clearBodiesDestroysRegisteredBackendBodiesAndJoints() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:clear-bodies-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - PhysicsBody first = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody second = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - RigidBodyKey firstId = resource.addBody(space.id(), - first, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - RigidBodyKey secondId = resource.addBody(space.id(), - second, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - PhysicsJoint fixedJoint = space.createFixedJoint(first, second, new Vector3f(), new Vector3f()); - resource.addJoint(space.id(), fixedJoint); - resource.markContinuousCollisionForced(firstId); - resource.markBodyControlled(firstId); - resource.updateChunkBoundarySafeState(firstId, new Vector3f(1.0f), new Quaternionf()); - resource.pauseChunkBoundaryBody(firstId, - 42L, - PhysicsBodyType.DYNAMIC, - new Vector3f(2.0f, 0.0f, 0.0f), - new Vector3f(0.0f, 3.0f, 0.0f)); - - assertEquals(2, resource.refreshBodySnapshots()); - assertEquals(2, space.bodyCount()); - assertEquals(1, space.jointCount()); - assertEquals(2, resource.getBodyRegistrationCount()); - - resource.clearBodies(); - - PhysicsSpace retainedSpace = resource.callOwner("resolve clear bodies space", - () -> resource.getLiveSpace(space.id())); - assertSame(space, retainedSpace); - assertEquals(0, space.bodyCount()); - assertEquals(0, space.jointCount()); - assertEquals(0, resource.getBodyRegistrationCount()); - assertEquals(0, resource.getBodySnapshotCount()); - assertEquals(0, resource.getBodySnapshotCellCount()); - assertNull(resource.getBody(firstId)); - assertNull(resource.getBody(secondId)); - assertFalse(resource.isBodyControlled(firstId)); - assertNull(resource.getChunkBoundarySafeState(firstId)); - assertNull(resource.getChunkBoundaryPauseState(firstId)); - assertThrows(IllegalArgumentException.class, () -> resource.getBodySnapshot(firstId)); - assertForcedCcdRestoreDoesNotAffectReusedBodyId(resource, space, firstId); - } - - @Test - void bodySnapshotStoreRefreshesQueriesAndDropsStaleBodies() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:snapshot-store-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - PhysicsBody near = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - near.setPosition(0.0f, 0.0f, 0.0f); - PhysicsBody far = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - far.setPosition(40.0f, 0.0f, 0.0f); - RigidBodyKey nearId = resource.addBody(space.id(), - near, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - resource.addBody(space.id(), - far, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - - assertEquals(2, resource.refreshBodySnapshots()); - assertEquals(2, resource.getBodySnapshotCount(space.id())); - AtomicInteger nearMatches = new AtomicInteger(); - int candidates = resource.forEachBodySnapshotNear(space.id(), - new Vector3f(), - 8.0f, - entry -> { - assertEquals(nearId, entry.bodyKey()); - nearMatches.incrementAndGet(); - }); - assertEquals(1, candidates); - assertEquals(1, nearMatches.get()); - - space.removeBody(far); - assertEquals(1, resource.refreshBodySnapshots()); - - assertEquals(1, resource.getBodySnapshotCount()); - assertEquals(1, resource.getBodySnapshotCount(space.id())); - AtomicInteger remainingSnapshots = new AtomicInteger(); - resource.forEachBodySnapshot(space.id(), entry -> { - assertEquals(nearId, entry.bodyKey()); - remainingSnapshots.incrementAndGet(); - }); - assertEquals(1, remainingSnapshots.get()); - resource.clearBodies(); - assertEquals(0, resource.getBodySnapshotCount()); - assertEquals(0, resource.getBodySnapshotCellCount()); - } - - @Test - void getBodySnapshotFallsBackToLiveBackendWhenReaderSnapshotIsMissing() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:snapshot-live-fallback-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - body.setPosition(7.0f, 8.0f, 9.0f); - RigidBodyKey bodyId = resource.addBody(space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - PublishedPhysicsSnapshotFrame latestFrame = resource.getLatestPublishedFrame(); - resource.applyPublishedSnapshotFrame(PublishedPhysicsSnapshotFrame.empty( - latestFrame.frameEpoch() + 1L, - latestFrame.worldEpoch())); - - assertEquals(0, resource.getBodySnapshotCount()); - - PhysicsBodySnapshot requiredSnapshot = resource.getBodySnapshot(bodyId); - PhysicsBodySnapshot optionalSnapshot = resource.getBodySnapshotIfRegistered(bodyId); - - assertEquals(new Vector3f(7.0f, 8.0f, 9.0f), requiredSnapshot.position()); - assertNotNull(optionalSnapshot); - assertEquals(new Vector3f(7.0f, 8.0f, 9.0f), optionalSnapshot.position()); - assertEquals(0, resource.getBodySnapshotCount()); - } - - @Test - void bodySnapshotStoreIgnoresUnregisteredSpaceBodies() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:snapshot-store-unregistered-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - PhysicsBody registered = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - registered.setPosition(0.0f, 0.0f, 0.0f); - PhysicsBody unregistered = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - unregistered.setPosition(2.0f, 0.0f, 0.0f); - RigidBodyKey registeredId = resource.addBody(space.id(), - registered, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - space.addBody(unregistered); - - assertEquals(1, resource.refreshBodySnapshots()); - assertEquals(1, resource.getBodySnapshotCount(space.id())); - - AtomicInteger snapshots = new AtomicInteger(); - resource.forEachBodySnapshot(space.id(), entry -> { - assertEquals(registeredId, entry.bodyKey()); - assertEquals(PhysicsBodyKind.BODY, entry.kind()); - assertEquals(PhysicsBodyPersistenceMode.PERSISTENT, entry.persistenceMode()); - snapshots.incrementAndGet(); - }); - assertEquals(1, snapshots.get()); - } - - @Test - void asyncBodyAddQueuesWithoutImmediateRegistration() throws Exception { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:async-body-add-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("async-body-add"); - resource.attachOwnerExecutor(owner); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - AtomicReference bodyRef = new AtomicReference<>(); - owner.submitAndDrain(() -> { - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - body.setPosition(1.0f, 2.0f, 3.0f); - bodyRef.set(body); - return PhysicsOwnerSnapshot.empty(); - }); - - CountDownLatch blockerStarted = new CountDownLatch(1); - CountDownLatch releaseBlocker = new CountDownLatch(1); - owner.submitMutation("block async topology", () -> { - blockerStarted.countDown(); - assertTrue(releaseBlocker.await(2, TimeUnit.SECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(blockerStarted.await(2, TimeUnit.SECONDS)); - - RigidBodyKey bodyId = RigidBodyKey.random(); - PhysicsMutationHandle handle = resource.addBodyAsync(bodyId, - space.id(), - bodyRef.get(), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - - assertEquals(bodyId, handle.value()); - assertFalse(handle.isDone()); - assertNull(resource.getBodyRegistrationView(bodyId)); - assertFalse(resource.isBodyCreationPending(bodyId)); - - releaseBlocker.countDown(); - pollMutations(owner, 2); - - assertTrue(handle.completedSuccessfully()); - assertFalse(handle.failed()); - assertFalse(resource.isBodyCreationPending(bodyId)); - assertEquals(bodyId, handle.join()); - PhysicsBody registeredBody = resource.callOwner("read registered test body", - () -> resource.getBody(bodyId)); - assertSame(bodyRef.get(), registeredBody); - assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), - resource.getBodySnapshot(bodyId).position()); - resource.detachOwnerExecutor(owner); - } - } - - @Test - void asyncBodyAddHandleObservesOwnerFailureWithoutPublicationDrain() throws Exception { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:async-body-add-failure-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("async-body-add-failure"); - resource.attachOwnerExecutor(owner); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - AtomicReference bodyRef = new AtomicReference<>(); - owner.submitAndDrain(() -> { - bodyRef.set(space.createBox(0.5f, 0.5f, 0.5f, 1.0f)); - return PhysicsOwnerSnapshot.empty(); - }); - - RigidBodyKey bodyId = RigidBodyKey.random(); - PhysicsMutationHandle handle = resource.addBodyAsync(bodyId, - new SpaceId(Integer.MAX_VALUE), - bodyRef.get(), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - - ExecutionException thrown = assertThrows(ExecutionException.class, - () -> handle.completion().toCompletableFuture().get(2, TimeUnit.SECONDS)); - - assertEquals(bodyId, handle.value()); - assertInstanceOf(IllegalArgumentException.class, thrown.getCause()); - assertInstanceOf(IllegalArgumentException.class, handle.failure()); - assertTrue(handle.failed()); - assertFalse(handle.completedSuccessfully()); - assertThrows(IllegalArgumentException.class, handle::throwIfFailed); - assertNull(resource.getBodyRegistrationView(bodyId)); - assertFalse(resource.isBodyCreationPending(bodyId)); - assertEquals(1, owner.pendingMutations()); - - pollMutations(owner, 1); - resource.detachOwnerExecutor(owner); - } - } - - @Test - void runtimePhysicsOwnerRoutingRunsOnAttachedOwner() throws Exception { - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - Thread testThread = Thread.currentThread(); - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("runtime-owner-routing"); - resource.attachOwnerExecutor(owner); - - AtomicReference callThread = new AtomicReference<>(); - int value = resource.callOwner("runtime physics owner call", () -> { - assertTrue(owner.isOwnerContext()); - callThread.set(Thread.currentThread()); - return 42; - }); - - assertEquals(42, value); - assertNotSame(testThread, callThread.get()); - - AtomicReference mutationThread = new AtomicReference<>(); - PhysicsMutationHandle handle = resource.enqueueOwnerMutation( - "runtime physics owner mutation", - "reserved-value", - () -> { - assertTrue(owner.isOwnerContext()); - mutationThread.set(Thread.currentThread()); - }); - - assertEquals("reserved-value", handle.value()); - assertEquals("reserved-value", handle.completion().toCompletableFuture() - .get(2, TimeUnit.SECONDS)); - assertTrue(handle.completedSuccessfully()); - assertNotSame(testThread, mutationThread.get()); - pollMutations(owner, 1); - resource.detachOwnerExecutor(owner); - } - } - - @Test - void ownerRoutingRunsInlineWithoutOwnerAndAfterDetach() throws Exception { - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - Thread testThread = Thread.currentThread(); - - AtomicReference inlineMutationThread = new AtomicReference<>(); - resource.runOwnerMutation("inline mutation", () -> inlineMutationThread.set(Thread.currentThread())); - assertSame(testThread, inlineMutationThread.get()); - - Thread inlineCallThread = resource.callOwner("inline call", Thread::currentThread); - assertSame(testThread, inlineCallThread); - - AtomicReference inlineAsyncThread = new AtomicReference<>(); - PhysicsMutationHandle inlineHandle = resource.enqueueOwnerMutation("inline async", - "inline", - () -> inlineAsyncThread.set(Thread.currentThread())); - assertSame(testThread, inlineAsyncThread.get()); - assertTrue(inlineHandle.completedSuccessfully()); - assertEquals("inline", inlineHandle.join()); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("owner-routing-detach"); - resource.attachOwnerExecutor(owner); - Thread ownerCallThread = resource.callOwner("owner call", () -> { - assertTrue(owner.isOwnerContext()); - return Thread.currentThread(); - }); - assertNotSame(testThread, ownerCallThread); - - resource.detachOwnerExecutor(owner); - Thread detachedCallThread = resource.callOwner("detached inline call", - Thread::currentThread); - assertSame(testThread, detachedCallThread); - } - } - - @Test - void liveBackendAccessAssertionRejectsOutsidePhysicsOwner() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:owner-guard-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("owner-guard"); - resource.attachOwnerExecutor(owner); - - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - PhysicsBody body = resource.callOwner("create test body", () -> { - PhysicsBody created = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - resource.addBody(space.id(), - created, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - return created; - }); - - assertThrows(IllegalStateException.class, () -> resource.assertCanAccessLiveBackendDirectly( - "direct test access")); - - resource.runOwnerMutation("move test body", () -> { - resource.assertCanAccessLiveBackendDirectly("owner test access"); - body.setPosition(1.0f, 2.0f, 3.0f); - }); - assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), - resource.callOwner("read test body position", body::getPosition)); - - resource.detachOwnerExecutor(owner); - } - } - - @Test - void resetRuntimeStatePublishesEmptyFrameAndRejectsPreResetSnapshot() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:reset-snapshot-frame-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - RigidBodyKey bodyId = resource.addBody(space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - - PublishedPhysicsSnapshotFrame preResetFrame = resource.capturePublishedSnapshotFrame(10L, - 20L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 30L, - false); - assertEquals(1, preResetFrame.bodyCount()); - assertEquals(1, resource.getBodySnapshotCount()); - - PhysicsRuntimeResetResult reset = - resource.resetRuntimeStateKeepingSpaces("test-world"); - - PublishedPhysicsSnapshotFrame resetFrame = resource.getLatestPublishedFrame(); - assertEquals(1, reset.removedBodies()); - assertEquals(0, resource.getBodySnapshotCount()); - assertEquals(PublishedPhysicsSnapshotFrame.Status.EMPTY, resetFrame.status()); - assertEquals(0, resetFrame.bodyCount()); - assertTrue(resetFrame.worldEpoch() > preResetFrame.worldEpoch()); - - assertEquals(0, resource.applyPublishedSnapshotFrame(preResetFrame)); - assertEquals(0, resource.getBodySnapshotCount()); - assertNull(resource.getBodyRegistrationView(bodyId)); - } - - @Test - void inlineWorldEpochAndVisualInterestCountersDoNotLoseConcurrentUpdates() throws Exception { - int threads = 8; - int iterations = 500; - int expectedUpdates = threads * iterations; - - LegacyLiveHandleTestResource epochResource = new LegacyLiveHandleTestResource(); - runConcurrently(threads, iterations, epochResource::clearBodies); - - assertEquals(expectedUpdates, epochResource.getLatestPublishedFrame().worldEpoch()); - - LegacyLiveHandleTestResource visualInterestResource = new LegacyLiveHandleTestResource(); - Set ticks = ConcurrentHashMap.newKeySet(); - runConcurrently(threads, iterations, - () -> ticks.add(visualInterestResource.advanceVisualInterestTick())); - - assertEquals(expectedUpdates, ticks.size()); - assertEquals(expectedUpdates, ticks.stream().mapToLong(Long::longValue).max().orElseThrow()); - } - - private static void runConcurrently(int threads, - int iterations, - Runnable action) throws Exception { - ExecutorService executor = Executors.newFixedThreadPool(threads); - CountDownLatch start = new CountDownLatch(1); - List> futures = new ArrayList<>(threads); - try { - for (int thread = 0; thread < threads; thread++) { - futures.add(executor.submit(() -> { - assertTrue(start.await(2, TimeUnit.SECONDS)); - for (int iteration = 0; iteration < iterations; iteration++) { - action.run(); - } - return null; - })); - } - start.countDown(); - for (Future future : futures) { - future.get(5, TimeUnit.SECONDS); - } - } finally { - executor.shutdownNow(); - } - assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); - } - - @Nonnull - private static PhysicsBodySnapshot physicsSnapshot(@Nonnull PhysicsBodyType bodyType) { - return new PhysicsBodySnapshot(new Vector3f(), - new Quaternionf(), - new Vector3f(), - new Vector3f(), - bodyType, - false, - false, - 0.0f, - ShapeType.BOX, - new Vector3f(0.5f), - 0.0f, - 0.0f, - PhysicsAxis.Y); - } - - private static void assertForcedCcdRestoreDoesNotAffectReusedBodyId( - @Nonnull LegacyLiveHandleTestResource resource, - @Nonnull PhysicsSpace space, - @Nonnull RigidBodyKey bodyId) { - PhysicsBody replacement = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - replacement.setContinuousCollisionEnabled(true); - resource.addBody(bodyId, - space.id(), - replacement, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - PhysicsWorldSettings settings = resource.getWorldSettings(); - settings.setStepMode(PhysicsStepMode.FIXED); - resource.setWorldSettings(settings); - - new PhysicsOwnerStepCommand(resource, 0.05f, false).run(); - - assertTrue(replacement.isContinuousCollisionEnabled()); - resource.destroyBody(bodyId); - } - - private static void pollMutations(TestPhysicsOwnerLane owner, - int expected) throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2L); - int completed = 0; - while (System.nanoTime() < deadline) { - completed += owner.pollCompletedMutations(8).size(); - if (completed >= expected) { - return; - } - Thread.sleep(10L); - } - assertEquals(expected, completed); - } - - private static final class ThrowingSyncCleanupResource extends LegacyLiveHandleTestResource { - - @Override - public void clearBodySyncState(@Nonnull Ref entityRef) { - throw new IllegalStateException("sync cleanup failed"); - } - } - - private static final class TestRef extends Ref { - - private final boolean valid; - - private TestRef(boolean valid) { - super(null); - this.valid = valid; - } - - @Override - public boolean isValid() { - return valid; - } - } - -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldRuntimeResourceTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldRuntimeResourceTest.java deleted file mode 100644 index a3f6336a..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldRuntimeResourceTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources; - -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertSame; - -import com.hypixel.hytale.component.ComponentRegistry; -import com.hypixel.hytale.component.EmptyResourceStorage; -import com.hypixel.hytale.component.ResourceType; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import javax.annotation.Nonnull; -import org.junit.jupiter.api.Test; - -class PhysicsWorldRuntimeResourceTest { - - @Test - void registersRuntimeImplementationBehindPublicResourceType() { - ComponentRegistry registry = new ComponentRegistry<>(); - ResourceType resourceType = - registry.registerResource(PhysicsWorldResource.class, - PhysicsWorldRuntimeResource::new); - - assertSame(PhysicsWorldResource.class, resourceType.getTypeClass()); - - Store store = registry.addStore(testEntityStore("runtime-resource-test"), - EmptyResourceStorage.get()); - PhysicsWorldResource resource = store.getResource(resourceType); - - PhysicsWorldRuntimeResource runtime = - assertInstanceOf(PhysicsWorldRuntimeResource.class, resource); - assertSame(runtime, PhysicsWorldRuntimeResource.require(resource)); - - registry.removeStore(store); - registry.shutdown(); - } - - @Nonnull - private static EntityStore testEntityStore(@Nonnull String worldName) { - return new EntityStore(TestInstanceFactory.world(worldName)); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldSettingsTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldSettingsTest.java index 8476c74c..3b26e6f9 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldSettingsTest.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/PhysicsWorldSettingsTest.java @@ -3,7 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsEventCollectionMode; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventCollectionMode; import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepSchedulingMode; import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyRegistryTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyRegistryTest.java deleted file mode 100644 index 91bcae63..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodyRegistryTest.java +++ /dev/null @@ -1,103 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.body; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyRegistrationView; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import java.util.ArrayList; -import java.util.List; -import javax.annotation.Nonnull; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -class PhysicsBodyRegistryTest { - - @Test - void indexesRegistrationsBySpaceWithoutScanningUnrelatedSpaces() { - SpaceId firstSpace = new SpaceId(1); - SpaceId secondSpace = new SpaceId(2); - RigidBodyKey firstId = RigidBodyKey.of(0L, 1L); - RigidBodyKey secondId = RigidBodyKey.of(0L, 2L); - PhysicsBodyRegistry registry = new PhysicsBodyRegistry(); - - registry.registerBody(firstId, - handle(11L), - firstSpace, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - registry.registerBody(secondId, - handle(12L), - secondSpace, - PhysicsBodyKind.TEMPORARY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - - List firstSpaceIds = new ArrayList<>(); - registry.forEachRegistration(firstSpace, - registration -> firstSpaceIds.add(registration.bodyKey())); - - assertEquals(List.of(firstId), firstSpaceIds); - assertEquals(1, registry.getRegistrationCount(firstSpace)); - assertEquals(1, registry.getRegistrationCount(secondSpace)); - - registry.unregisterBody(firstId); - - assertEquals(0, registry.getRegistrationCount(firstSpace)); - assertEquals(1, registry.getRegistrationCount(secondSpace)); - } - - @Test - void reRegisteringSameBodyWithDifferentSpaceIsRejectedWithoutMovingIndex() { - SpaceId firstSpace = new SpaceId(1); - SpaceId secondSpace = new SpaceId(2); - RigidBodyKey bodyId = RigidBodyKey.of(0L, 3L); - PhysicsBodyRegistry registry = new PhysicsBodyRegistry(); - registry.registerBody(bodyId, - handle(21L), - firstSpace, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - - assertThrows(IllegalArgumentException.class, () -> registry.registerBody(bodyId, - handle(21L), - secondSpace, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY)); - - assertEquals(1, registry.getRegistrationCount(firstSpace)); - assertEquals(0, registry.getRegistrationCount(secondSpace)); - } - - @Test - void registrationViewsReuseCachedImmutableMetadata() { - SpaceId space = new SpaceId(1); - RigidBodyKey bodyId = RigidBodyKey.of(0L, 4L); - PhysicsBodyRegistry registry = new PhysicsBodyRegistry(); - registry.registerBody(bodyId, - handle(31L), - space, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - - PhysicsBodyRegistrationView first = registry.getRegistrationView(bodyId); - PhysicsBodyRegistrationView second = registry.getRegistrationView(bodyId); - PhysicsBodyRegistrationView fromCollection = registry.getRegistrationViews() - .iterator() - .next(); - - assertSame(first, second); - assertSame(first, fromCollection); - Assertions.assertNotNull(first); - assertEquals(bodyId, first.bodyKey()); - } - - @Nonnull - private static BackendBodyHandle handle(long value) { - return new BackendBodyHandle(value); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodySnapshotStoreTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodySnapshotStoreTest.java deleted file mode 100644 index 25f3e731..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/body/PhysicsBodySnapshotStoreTest.java +++ /dev/null @@ -1,410 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.body; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsContact; -import dev.hytalemodding.impulse.api.PhysicsJoint; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.PhysicsRayHit; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsBodySnapshot; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSpaceFrame; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import java.util.function.BiConsumer; -import java.util.function.Function; -import javax.annotation.Nonnull; -import org.joml.Quaternionf; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class PhysicsBodySnapshotStoreTest { - - @Test - void refreshPassesLazySelectedBodiesToBackend() { - FakePhysicsBackend backend = new FakePhysicsBackend("test:snapshot-store-lazy-refresh"); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace delegate = resource.createLiveSpace(backend.getId(), "test-world"); - PhysicsBody body = delegate.createBox(0.5f, 0.5f, 0.5f, 1.0f); - RigidBodyKey bodyId = RigidBodyKey.of(0L, 1L); - resource.addBody(bodyId, - delegate.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - PhysicsSpaceBinding binding = resource.requireSpaceBinding(delegate.id()); - PhysicsBodyRegistry registry = new PhysicsBodyRegistry(); - registry.registerBody(bodyId, - resource.requireBodyRegistration(bodyId).backendBodyHandle(), - delegate.id(), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - PhysicsBodySnapshotStore store = new PhysicsBodySnapshotStore(); - - assertEquals(1, store.refresh(List.of(binding), registry)); - - assertEquals(1, store.bodyCount()); - } - - @Test - void appliesPublishedFramesIncrementallyWithoutReinsertingUnchangedBodies() { - SpaceId spaceId = new SpaceId(1); - RigidBodyKey bodyId = RigidBodyKey.of(0L, 1L); - PhysicsBodySnapshotStore store = new PhysicsBodySnapshotStore(); - - PhysicsBodySnapshotStore.ApplyStats firstApply = store.applyPublishedFrame( - frame(spaceId, bodyId, 1L, new Vector3f(1.0f, 2.0f, 3.0f))); - PhysicsBodySnapshotStore.ApplyStats secondApply = store.applyPublishedFrame( - frame(spaceId, bodyId, 2L, new Vector3f(2.0f, 2.0f, 3.0f))); - - assertEquals(1, firstApply.applied()); - assertEquals(1, firstApply.inserted()); - assertEquals(0, firstApply.removed()); - assertEquals(1, secondApply.applied()); - assertEquals(0, secondApply.inserted()); - assertEquals(0, secondApply.removed()); - assertEquals(1, store.bodyCount()); - assertEquals(1, store.bodyCount(spaceId)); - assertEquals(1, store.cellCount()); - } - - @Test - void applyPublishedFrameUsesFrameMetadataWithoutLiveRegistry() { - SpaceId spaceId = new SpaceId(1); - RigidBodyKey bodyId = RigidBodyKey.of(0L, 12L); - PhysicsBodySnapshotStore store = new PhysicsBodySnapshotStore(); - - PhysicsBodySnapshotStore.ApplyStats apply = store.applyPublishedFrame( - frame(spaceId, bodyId, 1L, new Vector3f(1.0f, 2.0f, 3.0f))); - - assertEquals(1, apply.applied()); - assertEquals(1, apply.inserted()); - assertEquals(1, store.bodyCount()); - assertEquals(1, store.bodyCount(spaceId)); - } - - @Test - void applyPublishedFrameReusesSnapshotWhenBodyStateIsUnchanged() { - SpaceId spaceId = new SpaceId(1); - RigidBodyKey bodyId = RigidBodyKey.of(0L, 2L); - PhysicsBodySnapshotStore store = new PhysicsBodySnapshotStore(); - - store.applyPublishedFrame(frame(spaceId, bodyId, 1L, new Vector3f(1.0f, 2.0f, 3.0f))); - var firstSnapshot = store.get(bodyId); - store.applyPublishedFrame(frame(spaceId, bodyId, 2L, new Vector3f(1.0f, 2.0f, 3.0f))); - - assertSame(firstSnapshot, store.get(bodyId)); - } - - @Test - void internalNearVisitorExposesSnapshotMetadataWithoutEntryDto() { - SpaceId spaceId = new SpaceId(1); - RigidBodyKey nearBodyId = RigidBodyKey.of(0L, 10L); - RigidBodyKey farBodyId = RigidBodyKey.of(0L, 11L); - PhysicsBodySnapshot nearSnapshot = snapshotAt(1.0f, 2.0f, 3.0f); - PhysicsBodySnapshot farSnapshot = snapshotAt(100.0f, 2.0f, 3.0f); - PhysicsBodySnapshotStore store = new PhysicsBodySnapshotStore(); - store.put(nearBodyId, - nearSnapshot, - spaceId, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - store.put(farBodyId, - farSnapshot, - spaceId, - PhysicsBodyKind.TEMPORARY, - PhysicsBodyPersistenceMode.PERSISTENT); - - List visited = new ArrayList<>(); - int candidates = store.forEachIndexedNear(spaceId, - new Vector3f(0.0f, 2.0f, 3.0f), - 4.0f, - (bodyId, snapshot, bodySpaceId, kind, persistenceMode) -> { - visited.add(bodyId); - assertSame(nearSnapshot, snapshot); - assertEquals(spaceId, bodySpaceId); - assertEquals(PhysicsBodyKind.BODY, kind); - assertEquals(PhysicsBodyPersistenceMode.RUNTIME_ONLY, persistenceMode); - }); - - assertEquals(1, candidates); - assertEquals(List.of(nearBodyId), visited); - } - - private static PublishedPhysicsSnapshotFrame frame(SpaceId spaceId, - RigidBodyKey bodyId, - long frameEpoch, - Vector3f position) { - PublishedPhysicsBodySnapshot body = new PublishedPhysicsBodySnapshot(bodyId, - spaceId, - frameEpoch, - 0L, - 0L, - 0L, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY, - position, - new Quaternionf(), - new Vector3f(), - new Vector3f(), - PhysicsBodyType.DYNAMIC, - false, - false, - 0.0f, - ShapeType.BOX, - new Vector3f(0.5f, 0.5f, 0.5f), - 0.0f, - 0.0f, - PhysicsAxis.Y); - return new PublishedPhysicsSnapshotFrame(frameEpoch, - 0L, - frameEpoch, - frameEpoch, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 1, - 0L, - 0L, - List.of(new PublishedPhysicsSpaceFrame(spaceId, frameEpoch, 0L, 0L, List.of(body)))); - } - - private static PhysicsBodySnapshot snapshotAt(float x, float y, float z) { - return new PhysicsBodySnapshot(new Vector3f(x, y, z), - new Quaternionf(), - new Vector3f(), - new Vector3f(), - PhysicsBodyType.DYNAMIC, - false, - false, - 0.0f, - ShapeType.BOX, - new Vector3f(0.5f, 0.5f, 0.5f), - 0.0f, - 0.0f, - PhysicsAxis.Y); - } - - @Nonnull - private static BackendBodyHandle handle(long value) { - return new BackendBodyHandle(value); - } - - private static final class RecordingSnapshotSpace implements PhysicsSpace { - - private final PhysicsSpace delegate; - private int selectedBodyCount; - - private RecordingSnapshotSpace(@Nonnull PhysicsSpace delegate) { - this.delegate = delegate; - } - - @Nonnull - @Override - public SpaceId id() { - return delegate.id(); - } - - @Nonnull - @Override - public BackendId backendId() { - return delegate.backendId(); - } - - @Override - public void step(float dt) { - delegate.step(dt); - } - - @Override - public void setGravity(float x, float y, float z) { - delegate.setGravity(x, y, z); - } - - @Nonnull - @Override - public Vector3f getGravity() { - return delegate.getGravity(); - } - - @Override - public void addBody(@Nonnull PhysicsBody body) { - delegate.addBody(body); - } - - @Override - public void removeBody(@Nonnull PhysicsBody body) { - delegate.removeBody(body); - } - - @Nonnull - @Override - public List getBodies() { - return delegate.getBodies(); - } - - @Override - public boolean containsBody(@Nonnull PhysicsBody body) { - return delegate.containsBody(body); - } - - @Override - public void snapshotBodies(@Nonnull Iterable selectedBodies, - @Nonnull Function previousSnapshots, - @Nonnull BiConsumer consumer) { - for (PhysicsBody body : selectedBodies) { - selectedBodyCount++; - consumer.accept(body, PhysicsBodySnapshot.from(body, previousSnapshots.apply(body))); - } - } - - @Nonnull - @Override - public PhysicsBody createStaticPlane(float groundY) { - return delegate.createStaticPlane(groundY); - } - - @Nonnull - @Override - public PhysicsBody createBox(float halfX, float halfY, float halfZ, float mass) { - return delegate.createBox(halfX, halfY, halfZ, mass); - } - - @Nonnull - @Override - public PhysicsBody createBox(@Nonnull Vector3f halfExtents, float mass) { - return delegate.createBox(halfExtents, mass); - } - - @Nonnull - @Override - public PhysicsBody createSphere(float radius, float mass) { - return delegate.createSphere(radius, mass); - } - - @Nonnull - @Override - public PhysicsBody createCapsule(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - return delegate.createCapsule(radius, halfHeight, axis, mass); - } - - @Nonnull - @Override - public PhysicsBody createCylinder(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - return delegate.createCylinder(radius, halfHeight, axis, mass); - } - - @Nonnull - @Override - public PhysicsBody createCone(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - return delegate.createCone(radius, halfHeight, axis, mass); - } - - @Nonnull - @Override - public Optional raycastClosest(@Nonnull Vector3f from, @Nonnull Vector3f to) { - return delegate.raycastClosest(from, to); - } - - @Nonnull - @Override - public List raycastAll(@Nonnull Vector3f from, @Nonnull Vector3f to) { - return delegate.raycastAll(from, to); - } - - @Nonnull - @Override - public List getContacts() { - return delegate.getContacts(); - } - - @Nonnull - @Override - public PhysicsJoint createFixedJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB) { - return delegate.createFixedJoint(bodyA, bodyB, anchorA, anchorB); - } - - @Nonnull - @Override - public PhysicsJoint createPointJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB) { - return delegate.createPointJoint(bodyA, bodyB, anchorA, anchorB); - } - - @Nonnull - @Override - public PhysicsJoint createHingeJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis) { - return delegate.createHingeJoint(bodyA, bodyB, anchorA, anchorB, axis); - } - - @Nonnull - @Override - public PhysicsJoint createSliderJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis) { - return delegate.createSliderJoint(bodyA, bodyB, anchorA, anchorB, axis); - } - - @Nonnull - @Override - public PhysicsJoint createSpringJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - float restLength, - float stiffness, - float damping) { - return delegate.createSpringJoint(bodyA, bodyB, anchorA, anchorB, restLength, stiffness, damping); - } - - @Override - public void removeJoint(@Nonnull PhysicsJoint joint) { - delegate.removeJoint(joint); - } - - @Nonnull - @Override - public List getJoints() { - return delegate.getJoints(); - } - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/lifecycle/PhysicsWorldLifecycleStateTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/lifecycle/PhysicsWorldLifecycleStateTest.java deleted file mode 100644 index 31667417..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/lifecycle/PhysicsWorldLifecycleStateTest.java +++ /dev/null @@ -1,175 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.lifecycle; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.TestPhysicsOwnerLane; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsSnapshotPublicationEvent; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandResult; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import java.time.Duration; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.Test; - -class PhysicsWorldLifecycleStateTest { - - private static final AtomicInteger BACKEND_COUNTER = new AtomicInteger(); - - @Test - void stalePublishedFrameIsRejectedAfterWorldEpochChanges() { - Fixture fixture = createFixture("stale-frame"); - RigidBodyKey bodyId = registerBox(fixture); - PublishedPhysicsSnapshotFrame staleFrame = fixture.resource.capturePublishedSnapshotFrame(10L, - 20L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - - fixture.resource.clearBodies(); - - assertEquals(0, fixture.resource.applyPublishedSnapshotFrame(staleFrame)); - assertEquals(0, fixture.resource.getBodySnapshotCount()); - assertNull(fixture.resource.getBodyRegistrationView(bodyId)); - assertEquals(0, fixture.resource.getLatestEventFrame().snapshotPublicationCount()); - } - - @Test - void currentPublishedFrameAppliesReaderSnapshotState() { - Fixture fixture = createFixture("current-frame"); - RigidBodyKey bodyId = registerBox(fixture); - long appliedBefore = fixture.resource.getLatestSnapshotAppliedNanos(); - - PublishedPhysicsSnapshotFrame frame = fixture.resource.capturePublishedSnapshotFrame(11L, - 21L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - - assertEquals(1, fixture.resource.applyPublishedSnapshotFrame(frame)); - assertEquals(1, fixture.resource.getBodySnapshotCount()); - assertNotNull(fixture.resource.getBodySnapshot(bodyId)); - assertTrue(fixture.resource.getLatestSnapshotAppliedNanos() >= appliedBefore); - } - - @Test - void commandRegistrationViewsPublishOnlyWhenCurrentFrameApplies() throws Exception { - Fixture fixture = createFixture("registration-view"); - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("lifecycle-registration-view"); - fixture.resource.attachOwnerExecutor(owner); - - RigidBodyKey bodyId = RigidBodyKey.random(); - var handle = fixture.resource.submitCommands(30L, commands -> commands - .spawnBody(bodyId, spawn -> spawn - .space(fixture.space.id()) - .box(0.5f, 0.5f, 0.5f) - .dynamic())); - handle.completion().toCompletableFuture().get(2, TimeUnit.SECONDS); - - assertNull(fixture.resource.getBodyRegistrationView(bodyId)); - assertTrue(fixture.resource.isBodyCreationPending(bodyId)); - - PublishedPhysicsSnapshotFrame frame = fixture.resource.capturePublishedSnapshotFrame(12L, - 31L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - - assertNull(fixture.resource.getBodyRegistrationView(bodyId)); - assertEquals(1, fixture.resource.applyPublishedSnapshotFrame(frame)); - assertNotNull(fixture.resource.getBodyRegistrationView(bodyId)); - assertFalse(fixture.resource.isBodyCreationPending(bodyId)); - - fixture.resource.detachOwnerExecutor(owner); - } - } - - @Test - void appliedFrameUpdatesCapturedSnapshotCommandInclusion() { - Fixture fixture = createFixture("command-last-included"); - PhysicsCommandResult result = fixture.resource.submitCommands(40L, - commands -> commands.setSpaceGravity(fixture.space.id(), 0.0f, -4.0f, 0.0f)) - .completion() - .toCompletableFuture() - .join() - .getFirst(); - PublishedPhysicsSnapshotFrame frame = fixture.resource.capturePublishedSnapshotFrame(13L, - 41L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - - fixture.resource.applyPublishedSnapshotFrame(frame); - PhysicsEventFrame eventFrame = fixture.resource.getLatestEventFrame(); - - assertTrue(frame.lastIncludedCommandBatchSequence() >= result.commandBatchSequence()); - assertTrue(eventFrame.latestCapturedSnapshotIncludesCommandBatch(result.commandBatchSequence())); - assertEquals(frame.lastIncludedCommandBatchSequence(), - eventFrame.latestCapturedSnapshotLastIncludedCommandBatchSequence()); - } - - @Test - void currentFramePublicationCreatesSnapshotPublicationEvent() { - Fixture fixture = createFixture("publication-event"); - registerBox(fixture); - PublishedPhysicsSnapshotFrame frame = fixture.resource.capturePublishedSnapshotFrame(14L, - 42L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - - int applied = fixture.resource.applyPublishedSnapshotFrame(frame, 43L); - PhysicsEventFrame eventFrame = fixture.resource.getLatestEventFrame(); - PhysicsSnapshotPublicationEvent event = eventFrame.latestSnapshotPublication(); - - assertEquals(1, applied); - assertEquals(1, eventFrame.snapshotPublicationCount()); - assertNotNull(event); - assertEquals(frame.frameEpoch(), event.snapshotFrameEpoch()); - assertEquals(frame.worldEpoch(), event.worldEpoch()); - assertEquals(frame.stepSequence(), event.stepSequence()); - assertEquals(frame.serverTick(), event.serverTick()); - assertEquals(frame.lastIncludedCommandBatchSequence(), event.lastIncludedCommandBatchSequence()); - assertEquals(43L, event.publicationServerTick()); - assertTrue(event.publicationNanoTime() > 0L); - assertEquals(applied, event.appliedBodyCount()); - } - - private static Fixture createFixture(String name) { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:lifecycle-" + name + "-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpace space = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - return new Fixture(resource, space); - } - - private static RigidBodyKey registerBox(Fixture fixture) { - PhysicsBody body = fixture.space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - return fixture.resource.addBody(fixture.space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - } - - private record Fixture(LegacyLiveHandleTestResource resource, - PhysicsSpace space) { - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsCommandVisibilityStateTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsCommandVisibilityStateTest.java deleted file mode 100644 index 7d67932b..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsCommandVisibilityStateTest.java +++ /dev/null @@ -1,225 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.simulation.recorder.MutablePhysicsCommandContext; -import dev.hytalemodding.impulse.core.internal.simulation.batch.RecordedPhysicsCommandBatch; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import org.junit.jupiter.api.Test; - -class PhysicsCommandVisibilityStateTest { - - @Test - void singleSpawnBodyCreationUsesExactPendingKeyUntilSnapshotIncludesBatch() { - PhysicsCommandVisibilityState state = new PhysicsCommandVisibilityState(); - RigidBodyKey bodyKey = RigidBodyKey.random(); - RecordedPhysicsCommandBatch batch = singleSpawnBatch(7L, bodyKey); - - assertTrue(state.trackBodyCreationPublication(batch, true)); - - assertTrue(state.isBodyCreationPending(bodyKey, false)); - assertFalse(state.isBodyCreationPending(RigidBodyKey.random(), false)); - - state.applyLastIncludedCommandBatchSequence(7L); - - assertFalse(state.isBodyCreationPending(bodyKey, false)); - } - - @Test - void completedSpawnBodyCreationStaysPendingUntilSnapshotIncludesBatch() { - PhysicsCommandVisibilityState state = new PhysicsCommandVisibilityState(); - RigidBodyKey bodyKey = RigidBodyKey.random(); - RecordedPhysicsCommandBatch batch = singleSpawnBatch(8L, bodyKey); - - assertTrue(state.trackBodyCreationPublication(batch, true)); - - state.markCommandBatchCompleted(8L); - - assertEquals(8L, state.completedCommandBatchSequence()); - assertTrue(state.isBodyCreationPending(bodyKey, false)); - assertFalse(state.isBodyCreationPending(RigidBodyKey.random(), false)); - - state.applyLastIncludedCommandBatchSequence(7L); - - assertTrue(state.isBodyCreationPending(bodyKey, false)); - - state.applyLastIncludedCommandBatchSequence(8L); - - assertFalse(state.isBodyCreationPending(bodyKey, false)); - } - - @Test - void multiSingleSpawnBodyCreationTracksOnlySpawnedKeysUntilSnapshotIncludesBatch() { - PhysicsCommandVisibilityState state = new PhysicsCommandVisibilityState(); - RigidBodyKey firstBodyKey = RigidBodyKey.random(); - RigidBodyKey secondBodyKey = RigidBodyKey.random(); - RecordedPhysicsCommandBatch batch = multiSingleSpawnBatch(11L, firstBodyKey, secondBodyKey); - - assertTrue(state.trackBodyCreationPublication(batch, true)); - - assertTrue(state.isBodyCreationPending(firstBodyKey, false)); - assertTrue(state.isBodyCreationPending(secondBodyKey, false)); - assertFalse(state.isBodyCreationPending(RigidBodyKey.random(), false)); - - state.applyLastIncludedCommandBatchSequence(10L); - assertTrue(state.isBodyCreationPending(firstBodyKey, false)); - assertTrue(state.isBodyCreationPending(secondBodyKey, false)); - assertFalse(state.isBodyCreationPending(RigidBodyKey.random(), false)); - - state.applyLastIncludedCommandBatchSequence(11L); - assertFalse(state.isBodyCreationPending(firstBodyKey, false)); - assertFalse(state.isBodyCreationPending(secondBodyKey, false)); - } - - @Test - void spawnBatchBodyCreationTracksOnlySpawnedKeysUntilSnapshotIncludesBatch() { - PhysicsCommandVisibilityState state = new PhysicsCommandVisibilityState(); - RigidBodyKey firstBodyKey = RigidBodyKey.random(); - RigidBodyKey secondBodyKey = RigidBodyKey.random(); - RecordedPhysicsCommandBatch batch = spawnBatch(12L, firstBodyKey, secondBodyKey); - - assertTrue(state.trackBodyCreationPublication(batch, true)); - - assertTrue(state.isBodyCreationPending(firstBodyKey, false)); - assertTrue(state.isBodyCreationPending(secondBodyKey, false)); - assertFalse(state.isBodyCreationPending(RigidBodyKey.random(), false)); - - state.applyLastIncludedCommandBatchSequence(12L); - - assertFalse(state.isBodyCreationPending(firstBodyKey, false)); - assertFalse(state.isBodyCreationPending(secondBodyKey, false)); - } - - @Test - void templateSpawnBodyCreationTracksOnlySpawnedKeysUntilSnapshotIncludesBatch() { - PhysicsCommandVisibilityState state = new PhysicsCommandVisibilityState(); - RigidBodyKey firstBodyKey = RigidBodyKey.random(); - RigidBodyKey secondBodyKey = RigidBodyKey.random(); - RecordedPhysicsCommandBatch batch = templateSpawnBatch(13L, firstBodyKey, secondBodyKey); - - assertTrue(state.trackBodyCreationPublication(batch, true)); - - assertTrue(state.isBodyCreationPending(firstBodyKey, false)); - assertTrue(state.isBodyCreationPending(secondBodyKey, false)); - assertFalse(state.isBodyCreationPending(RigidBodyKey.random(), false)); - - state.applyLastIncludedCommandBatchSequence(13L); - - assertFalse(state.isBodyCreationPending(firstBodyKey, false)); - assertFalse(state.isBodyCreationPending(secondBodyKey, false)); - } - - @Test - void commandWorldEpochOnlyChangesOutsideCommandBatchExecution() { - PhysicsCommandVisibilityState state = new PhysicsCommandVisibilityState(); - - assertEquals(0L, state.commandWorldEpoch()); - assertTrue(state.markWorldChanged()); - assertEquals(1L, state.commandWorldEpoch()); - - int previousDepth = state.enterCommandBatchExecution(); - try { - assertFalse(state.markWorldChanged()); - assertEquals(1L, state.commandWorldEpoch()); - } finally { - state.exitCommandBatchExecution(previousDepth); - } - - assertTrue(state.markWorldChanged()); - assertEquals(2L, state.commandWorldEpoch()); - } - - @Test - void commandCompletionSequenceTracksMaxCompletedBatch() { - PhysicsCommandVisibilityState state = new PhysicsCommandVisibilityState(); - - state.markCommandBatchCompleted(4L); - state.markCommandBatchCompleted(2L); - - assertEquals(4L, state.completedCommandBatchSequence()); - } - - private static RecordedPhysicsCommandBatch singleSpawnBatch(long sequence, - RigidBodyKey bodyKey) { - MutablePhysicsCommandContext context = new MutablePhysicsCommandContext(1L, 0L); - context.spawnBody(bodyKey, spawn -> spawn - .space(new SpaceId(1)) - .box(0.5f, 0.5f, 0.5f) - .mass(1.0f) - .dynamic() - .kind(PhysicsBodyKind.BODY) - .persistence(PhysicsBodyPersistenceMode.RUNTIME_ONLY)); - return context.freezeInternal(sequence); - } - - private static RecordedPhysicsCommandBatch multiSingleSpawnBatch(long sequence, - RigidBodyKey firstBodyKey, - RigidBodyKey secondBodyKey) { - MutablePhysicsCommandContext context = new MutablePhysicsCommandContext(1L, 0L); - context.spawnBody(firstBodyKey, spawn -> spawn - .space(new SpaceId(1)) - .box(0.5f, 0.5f, 0.5f) - .mass(1.0f) - .dynamic() - .kind(PhysicsBodyKind.BODY) - .persistence(PhysicsBodyPersistenceMode.RUNTIME_ONLY)); - context.spawnBody(secondBodyKey, spawn -> spawn - .space(new SpaceId(1)) - .box(0.5f, 0.5f, 0.5f) - .mass(1.0f) - .dynamic() - .kind(PhysicsBodyKind.BODY) - .persistence(PhysicsBodyPersistenceMode.RUNTIME_ONLY)); - return context.freezeInternal(sequence); - } - - private static RecordedPhysicsCommandBatch spawnBatch(long sequence, - RigidBodyKey firstBodyKey, - RigidBodyKey secondBodyKey) { - MutablePhysicsCommandContext context = new MutablePhysicsCommandContext(1L, 0L); - context.spawnBodies(2, spawns -> { - spawns.body(firstBodyKey, spawn -> spawn - .space(new SpaceId(1)) - .box(0.5f, 0.5f, 0.5f) - .mass(1.0f) - .dynamic() - .kind(PhysicsBodyKind.BODY) - .persistence(PhysicsBodyPersistenceMode.RUNTIME_ONLY)); - spawns.body(secondBodyKey, spawn -> spawn - .space(new SpaceId(1)) - .box(0.5f, 0.5f, 0.5f) - .mass(1.0f) - .dynamic() - .kind(PhysicsBodyKind.BODY) - .persistence(PhysicsBodyPersistenceMode.RUNTIME_ONLY)); - }); - return context.freezeInternal(sequence); - } - - private static RecordedPhysicsCommandBatch templateSpawnBatch(long sequence, - RigidBodyKey firstBodyKey, - RigidBodyKey secondBodyKey) { - MutablePhysicsCommandContext context = new MutablePhysicsCommandContext(1L, 0L); - context.spawnBodies(2, - new SpaceId(1), - PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f), - 1.0f, - PhysicsBodyType.DYNAMIC, - RigidBodySpawnSettings.defaults(), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY, - spawns -> { - spawns.body(firstBodyKey, 0.0f, 0.0f, 0.0f); - spawns.body(secondBodyKey, 1.0f, 0.0f, 0.0f); - }); - return context.freezeInternal(sequence); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerGatewayTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerGatewayTest.java deleted file mode 100644 index 20b7c87a..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerGatewayTest.java +++ /dev/null @@ -1,246 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import java.util.Objects; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.atomic.AtomicInteger; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.junit.jupiter.api.Test; - -class PhysicsOwnerGatewayTest { - - @Test - void routesThroughInterfaceBackedOwnerExecutor() { - PhysicsOwnerGateway gateway = new PhysicsOwnerGateway(); - RecordingOwnerExecutor executor = new RecordingOwnerExecutor(); - AtomicInteger mutations = new AtomicInteger(); - - gateway.attachOwnerExecutor(executor); - - assertFalse(gateway.canAccessLiveBackendDirectly()); - - gateway.run("record routed mutation", mutations::incrementAndGet); - - assertEquals(1, mutations.get()); - assertEquals(1, executor.routedOperations()); - } - - @Test - void nestedOwnerCallsInlineOnlyForSameOwnerContext() { - PhysicsOwnerGateway gateway = new PhysicsOwnerGateway(); - RecordingOwnerExecutor executor = new RecordingOwnerExecutor(); - AtomicInteger nestedMutations = new AtomicInteger(); - - gateway.attachOwnerExecutor(executor); - - gateway.run("outer mutation", () -> { - assertTrue(gateway.canAccessLiveBackendDirectly()); - gateway.run("nested mutation", nestedMutations::incrementAndGet); - }); - - assertEquals(1, nestedMutations.get()); - assertEquals(1, executor.routedOperations()); - } - - @Test - void routesCallAndAsyncOperationsThroughInterfaceBackedOwnerExecutor() throws Exception { - PhysicsOwnerGateway gateway = new PhysicsOwnerGateway(); - RecordingOwnerExecutor executor = new RecordingOwnerExecutor(); - AtomicInteger mutations = new AtomicInteger(); - - gateway.attachOwnerExecutor(executor); - - PhysicsMutationHandle mutation = gateway.enqueue("record async mutation", - "done", - mutations::incrementAndGet); - CompletableFuture asyncCall = gateway.enqueueCall("record async call", - mutations::incrementAndGet); - int syncCall = gateway.call("record sync call", mutations::incrementAndGet); - - assertEquals("done", mutation.join()); - assertEquals(2, asyncCall.get()); - assertEquals(3, syncCall); - assertEquals(3, mutations.get()); - assertEquals(3, executor.routedOperations()); - } - - @Test - void detachOwnerExecutorRestoresDirectExecution() { - PhysicsOwnerGateway gateway = new PhysicsOwnerGateway(); - RecordingOwnerExecutor executor = new RecordingOwnerExecutor(); - AtomicInteger mutations = new AtomicInteger(); - - gateway.attachOwnerExecutor(executor); - gateway.detachOwnerExecutor(executor); - - assertTrue(gateway.canAccessLiveBackendDirectly()); - - gateway.run("direct mutation after detach", mutations::incrementAndGet); - - assertEquals(1, mutations.get()); - assertEquals(0, executor.routedOperations()); - } - - @Test - void routedFailuresPropagateThroughHandlesAndFutures() { - PhysicsOwnerGateway gateway = new PhysicsOwnerGateway(); - RecordingOwnerExecutor executor = new RecordingOwnerExecutor(); - - gateway.attachOwnerExecutor(executor); - - PhysicsMutationHandle mutation = gateway.enqueue("failing mutation", - "value", - () -> { - throw new IllegalStateException("boom"); - }); - CompletableFuture asyncCall = gateway.enqueueCall("failing call", - () -> { - throw new IllegalArgumentException("bad"); - }); - - assertTrue(mutation.failed()); - assertInstanceOf(IllegalStateException.class, mutation.failure()); - ExecutionException thrown = assertThrowsExecution(asyncCall); - assertInstanceOf(IllegalArgumentException.class, thrown.getCause()); - } - - @Test - void routedAsyncRejectionsReturnFailedHandlesAndFutures() { - PhysicsOwnerGateway gateway = new PhysicsOwnerGateway(); - RecordingOwnerExecutor executor = new RecordingOwnerExecutor(); - gateway.attachOwnerExecutor(executor); - executor.rejectAsync = true; - - PhysicsMutationHandle mutation = gateway.enqueue("rejected mutation", - "value", - () -> { - }); - CompletableFuture asyncCall = gateway.enqueueCall("rejected call", () -> "ignored"); - - assertTrue(mutation.failed()); - assertInstanceOf(RejectedExecutionException.class, mutation.failure()); - ExecutionException thrown = assertThrowsExecution(asyncCall); - assertInstanceOf(RejectedExecutionException.class, thrown.getCause()); - } - - @Test - void synchronousWaitGuardRejectsCompletionCallbackContext() { - PhysicsOwnerGateway gateway = new PhysicsOwnerGateway(); - RecordingOwnerExecutor executor = new RecordingOwnerExecutor(); - gateway.attachOwnerExecutor(executor); - - executor.completionCallbackContext = true; - - assertThrows(RejectedExecutionException.class, - () -> gateway.rejectSynchronousCompletionCallbackWait("release control session")); - } - - private static final class RecordingOwnerExecutor implements PhysicsOwnerExecutor { - - private final ThreadLocal ownerContext = ThreadLocal.withInitial(() -> false); - private final AtomicInteger routedOperations = new AtomicInteger(); - private boolean completionCallbackContext; - private boolean rejectAsync; - - @Override - public boolean isOwnerContext() { - return ownerContext.get(); - } - - @Override - public boolean isCompletionCallbackContext() { - return completionCallbackContext; - } - - @Override - public void run(@Nonnull String operation, - @Nonnull PhysicsOwnerMutation mutation) { - routedOperations.incrementAndGet(); - runInOwnerContext(() -> { - mutation.run(); - return null; - }); - } - - @Nonnull - @Override - public PhysicsMutationHandle enqueue(@Nonnull String operation, - @Nullable T value, - @Nonnull PhysicsOwnerMutation mutation) { - if (rejectAsync) { - throw new RejectedExecutionException("forced async rejection"); - } - try { - run(operation, mutation); - return PhysicsMutationHandle.completed(operation, value); - } catch (Throwable throwable) { - return PhysicsMutationHandle.failed(operation, value, throwable); - } - } - - @Nonnull - @Override - public CompletableFuture enqueueCall(@Nonnull String operation, - @Nonnull PhysicsOwnerCallable callable) { - if (rejectAsync) { - throw new RejectedExecutionException("forced async call rejection"); - } - try { - return CompletableFuture.completedFuture(call(operation, callable)); - } catch (Throwable throwable) { - CompletableFuture completion = new CompletableFuture<>(); - completion.completeExceptionally(throwable); - return completion; - } - } - - @Nonnull - @Override - public T call(@Nonnull String operation, - @Nonnull PhysicsOwnerCallable callable) { - routedOperations.incrementAndGet(); - return Objects.requireNonNull(runInOwnerContext(callable)); - } - - int routedOperations() { - return routedOperations.get(); - } - - @Nullable - private T runInOwnerContext(@Nonnull PhysicsOwnerCallable callable) { - boolean previous = ownerContext.get(); - ownerContext.set(true); - try { - return callable.call(); - } catch (RuntimeException exception) { - throw exception; - } catch (Exception exception) { - throw new IllegalStateException("owner operation failed", exception); - } finally { - ownerContext.set(previous); - } - } - } - - @Nonnull - private static ExecutionException assertThrowsExecution(@Nonnull CompletableFuture future) { - try { - future.get(); - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - throw new AssertionError("Interrupted while waiting for future", exception); - } catch (ExecutionException exception) { - return exception; - } - throw new AssertionError("Expected future to fail"); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerLaneSchedulerTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerLaneSchedulerTest.java deleted file mode 100644 index 0006e6d9..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerLaneSchedulerTest.java +++ /dev/null @@ -1,1236 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsFrameEvent; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import javax.annotation.Nonnull; -import org.junit.jupiter.api.Test; - -class PhysicsOwnerLaneSchedulerTest { - - private static final Duration CLOSE_TIMEOUT = Duration.ofMillis(250L); - private static final long SHORT_TIMEOUT_MILLIS = 100L; - private static final long TIMEOUT_MILLIS = 2_000L; - - @Test - void defaultPoolSizeLeavesCpuHeadroomForBackendSolvers() { - assertEquals(1, PhysicsOwnerLaneScheduler.DEFAULT_POOL_SIZE); - } - - @Test - void sameLaneSerializesQueuedWorkEvenWhenPoolHasMultipleThreads() throws Exception { - CountDownLatch firstStarted = new CountDownLatch(1); - CountDownLatch releaseFirst = new CountDownLatch(1); - CountDownLatch secondStarted = new CountDownLatch(1); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(2, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("same-lane"); - - lane.submitMutation("first", () -> { - firstStarted.countDown(); - assertTrue(releaseFirst.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(firstStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - lane.submitMutation("second", () -> { - secondStarted.countDown(); - return PhysicsOwnerSnapshot.empty(); - }); - - assertFalse(secondStarted.await(SHORT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - releaseFirst.countDown(); - assertTrue(secondStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertEquals(2, pollMutationCompletions(lane, 2).size()); - } finally { - releaseFirst.countDown(); - } - } - - @Test - void differentLanesRunInParallelWhenPoolSizeAllowsIt() throws Exception { - CountDownLatch laneOneStarted = new CountDownLatch(1); - CountDownLatch releaseLaneOne = new CountDownLatch(1); - CountDownLatch laneTwoStarted = new CountDownLatch(1); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(2, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource laneOne = scheduler.createLane(); - PhysicsOwnerLaneResource laneTwo = scheduler.createLane(); - laneOne.start("lane-one"); - laneTwo.start("lane-two"); - - laneOne.submitMutation("block lane one", () -> { - laneOneStarted.countDown(); - assertTrue(releaseLaneOne.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(laneOneStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - laneTwo.submitMutation("run lane two", () -> { - laneTwoStarted.countDown(); - return PhysicsOwnerSnapshot.empty(); - }); - - assertTrue(laneTwoStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - releaseLaneOne.countDown(); - assertEquals(1, pollMutationCompletions(laneOne, 1).size()); - assertEquals(1, pollMutationCompletions(laneTwo, 1).size()); - } finally { - releaseLaneOne.countDown(); - } - } - - @Test - void mutationsRunFifoWithinOneLane() throws Exception { - CountDownLatch activeStarted = new CountDownLatch(1); - CountDownLatch releaseActive = new CountDownLatch(1); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(2, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - List order = Collections.synchronizedList(new ArrayList<>()); - lane.start("mutation-fifo"); - - lane.submitMutation("active", () -> { - order.add("active"); - activeStarted.countDown(); - assertTrue(releaseActive.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(activeStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - lane.submitMutation("queued one", () -> { - order.add("queued-1"); - return PhysicsOwnerSnapshot.empty(); - }); - lane.submitMutation("queued two", () -> { - order.add("queued-2"); - return PhysicsOwnerSnapshot.empty(); - }); - - releaseActive.countDown(); - assertEquals(3, pollMutationCompletions(lane, 3).size()); - - assertEquals(List.of("active", "queued-1", "queued-2"), order); - } finally { - releaseActive.countDown(); - } - } - - @Test - void submittedStepDrainsPreCutoffMutationBacklogAfterActiveCommand() throws Exception { - CountDownLatch activeMutationStarted = new CountDownLatch(1); - CountDownLatch releaseActiveMutation = new CountDownLatch(1); - CountDownLatch preCutoffMutationStarted = new CountDownLatch(1); - CountDownLatch releasePreCutoffMutation = new CountDownLatch(1); - CountDownLatch postCutoffMutationStarted = new CountDownLatch(1); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(2, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("strict-pre-step-drain"); - - lane.submitMutation("active mutation", () -> { - activeMutationStarted.countDown(); - assertTrue(releaseActiveMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(activeMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - lane.submitMutation("pre-cutoff mutation", () -> { - preCutoffMutationStarted.countDown(); - assertTrue(releasePreCutoffMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - - PhysicsOwnerStepCommand step = new PhysicsOwnerStepCommand( - new LegacyLiveHandleTestResource(), - 0.05f, - false, - 1L, - 1L); - assertTrue(lane.submitStepIfIdle(step)); - - lane.submitMutation("post-cutoff mutation", () -> { - postCutoffMutationStarted.countDown(); - return PhysicsOwnerSnapshot.empty(); - }); - - releaseActiveMutation.countDown(); - assertTrue(preCutoffMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertNull(pollStepCompletion(lane, Duration.ofMillis(SHORT_TIMEOUT_MILLIS)), - "step must not complete before queued pre-cutoff mutations drain"); - assertFalse(postCutoffMutationStarted.await(SHORT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS), - "post-cutoff mutations must not run before the accepted step"); - - releasePreCutoffMutation.countDown(); - PhysicsOwnerStepCompletion completedStep = pollStepCompletion(lane, - Duration.ofMillis(TIMEOUT_MILLIS)); - - assertNotNull(completedStep, - "pre-cutoff mutations must drain before the accepted physics step"); - assertTrue(completedStep.completedSuccessfully()); - assertTrue(postCutoffMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertEquals(3, pollMutationCompletions(lane, 3).size()); - } finally { - releaseActiveMutation.countDown(); - releasePreCutoffMutation.countDown(); - } - } - - @Test - void submittedStepDrainsAllPreCutoffMutationsBeforePostCutoffBacklog() throws Exception { - CountDownLatch activeMutationStarted = new CountDownLatch(1); - CountDownLatch releaseActiveMutation = new CountDownLatch(1); - CountDownLatch firstPreCutoffStarted = new CountDownLatch(1); - CountDownLatch releaseFirstPreCutoff = new CountDownLatch(1); - CountDownLatch secondPreCutoffStarted = new CountDownLatch(1); - CountDownLatch releaseSecondPreCutoff = new CountDownLatch(1); - CountDownLatch postCutoffStarted = new CountDownLatch(1); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(2, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("strict-pre-step-drain-multiple"); - - lane.submitMutation("active mutation", () -> { - activeMutationStarted.countDown(); - assertTrue(releaseActiveMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(activeMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - lane.submitMutation("first pre-cutoff mutation", () -> { - firstPreCutoffStarted.countDown(); - assertTrue(releaseFirstPreCutoff.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - lane.submitMutation("second pre-cutoff mutation", () -> { - secondPreCutoffStarted.countDown(); - assertTrue(releaseSecondPreCutoff.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - - assertTrue(lane.submitStepIfIdle(new PhysicsOwnerStepCommand( - new LegacyLiveHandleTestResource(), - 0.05f, - false, - 1L, - 1L))); - - lane.submitMutation("post-cutoff mutation", () -> { - postCutoffStarted.countDown(); - return PhysicsOwnerSnapshot.empty(); - }); - - releaseActiveMutation.countDown(); - assertTrue(firstPreCutoffStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertNull(pollStepCompletion(lane, Duration.ofMillis(SHORT_TIMEOUT_MILLIS))); - assertFalse(postCutoffStarted.await(SHORT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - releaseFirstPreCutoff.countDown(); - assertTrue(secondPreCutoffStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertNull(pollStepCompletion(lane, Duration.ofMillis(SHORT_TIMEOUT_MILLIS))); - assertFalse(postCutoffStarted.await(SHORT_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - releaseSecondPreCutoff.countDown(); - PhysicsOwnerStepCompletion completedStep = pollStepCompletion(lane, - Duration.ofMillis(TIMEOUT_MILLIS)); - - assertNotNull(completedStep); - assertTrue(completedStep.completedSuccessfully()); - assertEquals(2, completedStep.preStepDrainedMutations()); - assertEquals(1, completedStep.lateMutationBacklogAtStep()); - assertTrue(postCutoffStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertEquals(4, pollMutationCompletions(lane, 4).size()); - } finally { - releaseActiveMutation.countDown(); - releaseFirstPreCutoff.countDown(); - releaseSecondPreCutoff.countDown(); - } - } - - @Test - void submittedStepDoesNotWaitForPostCutoffMutationBacklog() throws Exception { - CountDownLatch activeMutationStarted = new CountDownLatch(1); - CountDownLatch releaseActiveMutation = new CountDownLatch(1); - CountDownLatch postCutoffMutationStarted = new CountDownLatch(1); - CountDownLatch releasePostCutoffMutation = new CountDownLatch(1); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(2, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("post-cutoff-deferred"); - - lane.submitMutation("active mutation", () -> { - activeMutationStarted.countDown(); - assertTrue(releaseActiveMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(activeMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - PhysicsOwnerStepCommand step = new PhysicsOwnerStepCommand( - new LegacyLiveHandleTestResource(), - 0.05f, - false, - 1L, - 1L); - assertTrue(lane.submitStepIfIdle(step)); - - lane.submitMutation("post-cutoff mutation", () -> { - postCutoffMutationStarted.countDown(); - assertTrue(releasePostCutoffMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - - releaseActiveMutation.countDown(); - PhysicsOwnerStepCompletion completedStep = pollStepCompletion(lane, - Duration.ofMillis(TIMEOUT_MILLIS)); - - assertNotNull(completedStep, - "post-cutoff mutations must not delay an already-accepted physics step"); - assertTrue(completedStep.completedSuccessfully()); - assertTrue(postCutoffMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - releasePostCutoffMutation.countDown(); - assertEquals(2, pollMutationCompletions(lane, 2).size()); - } finally { - releaseActiveMutation.countDown(); - releasePostCutoffMutation.countDown(); - } - } - - @Test - void failedPreCutoffMutationIsReportedAndDoesNotPreventAcceptedStep() throws Exception { - CountDownLatch activeMutationStarted = new CountDownLatch(1); - CountDownLatch releaseActiveMutation = new CountDownLatch(1); - CountDownLatch failingPreCutoffStarted = new CountDownLatch(1); - RuntimeException expectedFailure = new RuntimeException("pre-cutoff boom"); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(2, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("strict-pre-step-drain-failure"); - - lane.submitMutation("active mutation", () -> { - activeMutationStarted.countDown(); - assertTrue(releaseActiveMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(activeMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - lane.submitMutation("failing pre-cutoff mutation", () -> { - failingPreCutoffStarted.countDown(); - throw expectedFailure; - }); - - assertTrue(lane.submitStepIfIdle(new PhysicsOwnerStepCommand( - new LegacyLiveHandleTestResource(), - 0.05f, - false, - 1L, - 1L))); - - releaseActiveMutation.countDown(); - assertTrue(failingPreCutoffStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - PhysicsOwnerStepCompletion completedStep = pollStepCompletion(lane, - Duration.ofMillis(TIMEOUT_MILLIS)); - - assertNotNull(completedStep); - assertTrue(completedStep.completedSuccessfully()); - assertEquals(1, completedStep.preStepDrainedMutations()); - assertTrue(completedStep.preStepDrainRunNanos() >= 0L); - - List completions = pollMutationCompletions(lane, 2); - assertEquals(2, completions.size()); - assertTrue(completions.get(0).completedSuccessfully()); - assertEquals(expectedFailure, completions.get(1).executionFailure()); - } finally { - releaseActiveMutation.countDown(); - } - } - - @Test - void completionCallbackCanWaitForQueuedMutationWithoutHoldingOwnerWorker() throws Exception { - CountDownLatch activeMutationStarted = new CountDownLatch(1); - CountDownLatch releaseActiveMutation = new CountDownLatch(1); - CountDownLatch preCutoffMutationStarted = new CountDownLatch(1); - CountDownLatch callbackFinished = new CountDownLatch(1); - AtomicReference callbackFailure = new AtomicReference<>(); - List order = Collections.synchronizedList(new ArrayList<>()); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(1, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("completion-callback-wait"); - - lane.submitMutation("active mutation", () -> { - order.add("active"); - activeMutationStarted.countDown(); - assertTrue(releaseActiveMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(activeMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - PhysicsMutationHandle preCutoff = lane.submitMutation("pre-cutoff mutation", - () -> { - order.add("pre"); - preCutoffMutationStarted.countDown(); - return PhysicsOwnerSnapshot.empty(); - }); - preCutoff.completion().whenComplete((ignored, failure) -> { - try { - assertNull(failure); - PhysicsMutationHandle callbackMutation = lane.enqueue("callback mutation", - null, - () -> order.add("callback")); - awaitHandle(callbackMutation); - } catch (Throwable throwable) { - callbackFailure.set(throwable); - } finally { - callbackFinished.countDown(); - } - }); - - assertTrue(lane.submitStepIfIdle(new PhysicsOwnerStepCommand( - new RecordingStepResource(order), - 0.05f, - false, - 1L, - 1L))); - - releaseActiveMutation.countDown(); - assertTrue(preCutoffMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertTrue(callbackFinished.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - PhysicsOwnerStepCompletion completedStep = pollStepCompletion(lane, - Duration.ofMillis(TIMEOUT_MILLIS)); - - assertNull(callbackFailure.get()); - assertNotNull(completedStep); - assertTrue(completedStep.completedSuccessfully()); - assertEquals(List.of("active", "pre", "step", "callback"), order); - assertEquals(3, pollMutationCompletions(lane, 3).size()); - } finally { - releaseActiveMutation.countDown(); - } - } - - @Test - void ownerBridgeCallAsyncCompletionRunsOutsideOwnerContextAndQueuesAfterAcceptedStep() - throws Exception { - CountDownLatch activeMutationStarted = new CountDownLatch(1); - CountDownLatch releaseActiveMutation = new CountDownLatch(1); - CountDownLatch bridgeCallStarted = new CountDownLatch(1); - CountDownLatch callbackObserved = new CountDownLatch(1); - CountDownLatch callbackMutationStarted = new CountDownLatch(1); - AtomicReference callbackFailure = new AtomicReference<>(); - AtomicReference completedValue = new AtomicReference<>(); - List order = Collections.synchronizedList(new ArrayList<>()); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(1, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("owner-bridge-call-async-completion"); - - lane.submitMutation("active mutation", () -> { - order.add("active"); - activeMutationStarted.countDown(); - assertTrue(releaseActiveMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(activeMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - CompletableFuture bridgeCall = PhysicsOwnerBridge.callAsync(lane, - "bridge async call", - () -> { - order.add("bridge"); - bridgeCallStarted.countDown(); - return "value"; - }); - bridgeCall.whenComplete((value, failure) -> { - try { - assertNull(failure); - assertFalse(lane.isOwnerContext()); - completedValue.set(value); - lane.enqueue("callback mutation", null, () -> { - order.add("callback"); - callbackMutationStarted.countDown(); - }); - } catch (Throwable throwable) { - callbackFailure.set(throwable); - } finally { - callbackObserved.countDown(); - } - }); - - assertTrue(lane.submitStepIfIdle(new PhysicsOwnerStepCommand( - new RecordingStepResource(order), - 0.05f, - false, - 1L, - 1L))); - - releaseActiveMutation.countDown(); - assertTrue(bridgeCallStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertTrue(callbackObserved.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - PhysicsOwnerStepCompletion completedStep = pollStepCompletion(lane, - Duration.ofMillis(TIMEOUT_MILLIS)); - - assertNull(callbackFailure.get()); - assertEquals("value", completedValue.get()); - assertNotNull(completedStep); - assertTrue(completedStep.completedSuccessfully()); - assertTrue(callbackMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertEquals(List.of("active", "bridge", "step", "callback"), order); - assertEquals(2, pollMutationCompletions(lane, 2).size()); - } finally { - releaseActiveMutation.countDown(); - } - } - - @Test - void enqueueCallCompletionRunsOutsideOwnerContextAndQueuesAfterAcceptedStep() - throws Exception { - CountDownLatch activeMutationStarted = new CountDownLatch(1); - CountDownLatch releaseActiveMutation = new CountDownLatch(1); - CountDownLatch callStarted = new CountDownLatch(1); - CountDownLatch callbackObserved = new CountDownLatch(1); - CountDownLatch callbackMutationStarted = new CountDownLatch(1); - AtomicReference callbackFailure = new AtomicReference<>(); - AtomicReference completedValue = new AtomicReference<>(); - List order = Collections.synchronizedList(new ArrayList<>()); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(1, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("enqueue-call-completion"); - - lane.submitMutation("active mutation", () -> { - order.add("active"); - activeMutationStarted.countDown(); - assertTrue(releaseActiveMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(activeMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - CompletableFuture call = lane.enqueueCall("queued call", () -> { - order.add("call"); - callStarted.countDown(); - return "value"; - }); - call.whenComplete((value, failure) -> { - try { - assertNull(failure); - assertFalse(lane.isOwnerContext()); - completedValue.set(value); - lane.enqueue("callback mutation", null, () -> { - order.add("callback"); - callbackMutationStarted.countDown(); - }); - } catch (Throwable throwable) { - callbackFailure.set(throwable); - } finally { - callbackObserved.countDown(); - } - }); - - assertTrue(lane.submitStepIfIdle(new PhysicsOwnerStepCommand( - new RecordingStepResource(order), - 0.05f, - false, - 1L, - 1L))); - - releaseActiveMutation.countDown(); - assertTrue(callStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertTrue(callbackObserved.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - PhysicsOwnerStepCompletion completedStep = pollStepCompletion(lane, - Duration.ofMillis(TIMEOUT_MILLIS)); - - assertNull(callbackFailure.get()); - assertEquals("value", completedValue.get()); - assertNotNull(completedStep); - assertTrue(completedStep.completedSuccessfully()); - assertTrue(callbackMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertEquals(List.of("active", "call", "step", "callback"), order); - assertEquals(2, pollMutationCompletions(lane, 2).size()); - } finally { - releaseActiveMutation.countDown(); - } - } - - @Test - void completionCallbackSubmittedMutationRunsAfterAcceptedStep() throws Exception { - CountDownLatch activeMutationStarted = new CountDownLatch(1); - CountDownLatch releaseActiveMutation = new CountDownLatch(1); - CountDownLatch preCutoffMutationStarted = new CountDownLatch(1); - CountDownLatch callbackObserved = new CountDownLatch(1); - CountDownLatch callbackMutationStarted = new CountDownLatch(1); - AtomicReference callbackFailure = new AtomicReference<>(); - List order = Collections.synchronizedList(new ArrayList<>()); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(2, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("completion-callback-reentrant-async"); - - lane.submitMutation("active mutation", () -> { - order.add("active"); - activeMutationStarted.countDown(); - assertTrue(releaseActiveMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(activeMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - PhysicsMutationHandle preCutoff = lane.submitMutation("pre-cutoff mutation", - () -> { - order.add("pre"); - preCutoffMutationStarted.countDown(); - return PhysicsOwnerSnapshot.empty(); - }); - preCutoff.completion().whenComplete((ignored, failure) -> { - try { - assertNull(failure); - assertFalse(lane.isOwnerContext()); - lane.enqueue("callback mutation", null, () -> { - order.add("callback"); - callbackMutationStarted.countDown(); - }); - } catch (Throwable throwable) { - callbackFailure.set(throwable); - } finally { - callbackObserved.countDown(); - } - }); - - assertTrue(lane.submitStepIfIdle(new PhysicsOwnerStepCommand( - new RecordingStepResource(order), - 0.05f, - false, - 1L, - 1L))); - - releaseActiveMutation.countDown(); - assertTrue(preCutoffMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertTrue(callbackObserved.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - PhysicsOwnerStepCompletion completedStep = pollStepCompletion(lane, - Duration.ofMillis(TIMEOUT_MILLIS)); - - assertNull(callbackFailure.get()); - assertNotNull(completedStep); - assertTrue(completedStep.completedSuccessfully()); - assertTrue(callbackMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - assertEquals(List.of("active", "pre", "step", "callback"), order); - assertEquals(3, pollMutationCompletions(lane, 3).size()); - } finally { - releaseActiveMutation.countDown(); - } - } - - @Test - void completionCallbackSynchronousOwnerWaitIsRejected() throws Exception { - CountDownLatch activeMutationStarted = new CountDownLatch(1); - CountDownLatch releaseActiveMutation = new CountDownLatch(1); - CountDownLatch callbackObserved = new CountDownLatch(1); - AtomicReference syncFailure = new AtomicReference<>(); - AtomicReference callFailure = new AtomicReference<>(); - AtomicReference drainFailure = new AtomicReference<>(); - AtomicReference crossLaneSyncFailure = new AtomicReference<>(); - AtomicBoolean callRan = new AtomicBoolean(); - AtomicBoolean drainRan = new AtomicBoolean(); - List order = Collections.synchronizedList(new ArrayList<>()); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(2, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - PhysicsOwnerLaneResource otherLane = scheduler.createLane(); - lane.start("completion-callback-sync-reject"); - otherLane.start("completion-callback-cross-sync-reject"); - - lane.submitMutation("active mutation", () -> { - order.add("active"); - activeMutationStarted.countDown(); - assertTrue(releaseActiveMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(activeMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - PhysicsMutationHandle preCutoff = lane.submitMutation("pre-cutoff mutation", - () -> { - order.add("pre"); - return PhysicsOwnerSnapshot.empty(); - }); - preCutoff.completion().whenComplete((ignored, failure) -> { - try { - assertNull(failure); - lane.run("sync callback mutation", () -> order.add("sync-callback")); - } catch (Throwable throwable) { - syncFailure.set(throwable); - } - try { - lane.call("sync callback call", () -> { - callRan.set(true); - return "rejected"; - }); - } catch (Throwable throwable) { - callFailure.set(throwable); - } - try { - lane.submitAndDrain(() -> { - drainRan.set(true); - return PhysicsOwnerSnapshot.empty(); - }); - } catch (Throwable throwable) { - drainFailure.set(throwable); - } - try { - otherLane.run("cross-lane sync callback mutation", - () -> order.add("cross-sync-callback")); - } catch (Throwable throwable) { - crossLaneSyncFailure.set(throwable); - } finally { - callbackObserved.countDown(); - } - }); - - assertTrue(lane.submitStepIfIdle(new PhysicsOwnerStepCommand( - new RecordingStepResource(order), - 0.05f, - false, - 1L, - 1L))); - - releaseActiveMutation.countDown(); - assertTrue(callbackObserved.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - PhysicsOwnerStepCompletion completedStep = pollStepCompletion(lane, - Duration.ofMillis(TIMEOUT_MILLIS)); - - assertNotNull(syncFailure.get()); - assertInstanceOf(RejectedExecutionException.class, syncFailure.get()); - assertNotNull(callFailure.get()); - assertInstanceOf(RejectedExecutionException.class, callFailure.get()); - assertFalse(callRan.get()); - assertNotNull(drainFailure.get()); - assertInstanceOf(RejectedExecutionException.class, drainFailure.get()); - assertFalse(drainRan.get()); - assertNotNull(crossLaneSyncFailure.get()); - assertInstanceOf(RejectedExecutionException.class, crossLaneSyncFailure.get()); - assertNotNull(completedStep); - assertTrue(completedStep.completedSuccessfully()); - assertEquals(List.of("active", "pre", "step"), order); - assertEquals(2, pollMutationCompletions(lane, 2).size()); - assertTrue(otherLane.pollCompletedMutations(1).isEmpty()); - } finally { - releaseActiveMutation.countDown(); - } - } - - @Test - void completionCallbackCloseIsRejectedWithoutClosingLane() throws Exception { - CountDownLatch activeMutationStarted = new CountDownLatch(1); - CountDownLatch releaseActiveMutation = new CountDownLatch(1); - CountDownLatch callbackObserved = new CountDownLatch(1); - AtomicReference callbackFailure = new AtomicReference<>(); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(1, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("completion-callback-close-reject"); - - lane.submitMutation("active mutation", () -> { - activeMutationStarted.countDown(); - assertTrue(releaseActiveMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(activeMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - PhysicsMutationHandle mutation = lane.submitMutation("complete then close", - PhysicsOwnerSnapshot::empty); - mutation.completion().whenComplete((ignored, failure) -> { - try { - assertNull(failure); - assertFalse(lane.isOwnerContext()); - assertThrows(RejectedExecutionException.class, lane::close); - } catch (Throwable throwable) { - callbackFailure.set(throwable); - } finally { - callbackObserved.countDown(); - } - }); - - releaseActiveMutation.countDown(); - assertTrue(callbackObserved.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - assertNull(callbackFailure.get()); - assertFalse(lane.isClosed()); - assertEquals(2, pollMutationCompletions(lane, 2).size()); - } finally { - releaseActiveMutation.countDown(); - } - } - - @Test - void schedulerCloseWaitsForCompletionCallbacks() throws Exception { - CountDownLatch callbackStarted = new CountDownLatch(1); - CountDownLatch releaseCallback = new CountDownLatch(1); - AtomicBoolean closeReturned = new AtomicBoolean(); - PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(1, - 8, - Duration.ofSeconds(2L)); - try { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("close-waits-for-completion"); - - PhysicsMutationHandle mutation = lane.submitMutation("blocked callback", - PhysicsOwnerSnapshot::empty); - mutation.completion().whenComplete((ignored, failure) -> { - callbackStarted.countDown(); - try { - assertTrue(releaseCallback.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - } - }); - - assertTrue(callbackStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - CompletableFuture close = CompletableFuture.runAsync(() -> { - scheduler.close(); - closeReturned.set(true); - }); - - assertFalse(closeReturned.get()); - assertFalse(close.isDone()); - - releaseCallback.countDown(); - close.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - - assertTrue(closeReturned.get()); - } finally { - releaseCallback.countDown(); - if (!closeReturned.get()) { - scheduler.close(); - } - } - } - - @Test - void laneCloseWaitsForCompletionCallbacks() throws Exception { - CountDownLatch callbackStarted = new CountDownLatch(1); - CountDownLatch releaseCallback = new CountDownLatch(1); - AtomicBoolean closeReturned = new AtomicBoolean(); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(1, - 8, - Duration.ofSeconds(2L))) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("lane-close-waits-for-completion"); - - PhysicsMutationHandle mutation = lane.submitMutation("blocked lane callback", - PhysicsOwnerSnapshot::empty); - mutation.completion().whenComplete((ignored, failure) -> { - callbackStarted.countDown(); - try { - assertTrue(releaseCallback.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - } - }); - - assertTrue(callbackStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - CompletableFuture close = CompletableFuture.runAsync(() -> { - lane.close(); - closeReturned.set(true); - }); - - assertFalse(closeReturned.get()); - assertFalse(close.isDone()); - - releaseCallback.countDown(); - close.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - - assertTrue(closeReturned.get()); - assertTrue(lane.isClosed()); - } finally { - releaseCallback.countDown(); - } - } - - @Test - void schedulerCloseFromCompletionCallbackIsRejected() throws Exception { - CountDownLatch mutationStarted = new CountDownLatch(1); - CountDownLatch releaseMutation = new CountDownLatch(1); - CountDownLatch callbackObserved = new CountDownLatch(1); - AtomicReference callbackFailure = new AtomicReference<>(); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(1, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("scheduler-close-callback-reject"); - - PhysicsMutationHandle mutation = lane.submitMutation("complete then scheduler close", - () -> { - mutationStarted.countDown(); - assertTrue(releaseMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(mutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - mutation.completion().whenComplete((ignored, failure) -> { - try { - assertNull(failure); - assertThrows(RejectedExecutionException.class, scheduler::close); - } catch (Throwable throwable) { - callbackFailure.set(throwable); - } finally { - callbackObserved.countDown(); - } - }); - - releaseMutation.countDown(); - assertTrue(callbackObserved.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - assertNull(callbackFailure.get()); - assertEquals(1, pollMutationCompletions(lane, 1).size()); - } finally { - releaseMutation.countDown(); - } - } - - @Test - void ownerContextCloseIsRejectedWithoutClosingLane() throws Exception { - AtomicBoolean rejectionObserved = new AtomicBoolean(); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(1, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("owner-context-close-reject"); - - PhysicsMutationHandle mutation = lane.enqueue("close from owner", null, () -> { - assertTrue(lane.isOwnerContext()); - assertThrows(RejectedExecutionException.class, lane::close); - rejectionObserved.set(true); - }); - - awaitHandle(mutation); - - assertTrue(rejectionObserved.get()); - assertFalse(lane.isClosed()); - assertEquals(1, pollMutationCompletions(lane, 1).size()); - } - } - - @Test - void completedStepReportsPreStepDrainBackpressure() throws Exception { - CountDownLatch activeMutationStarted = new CountDownLatch(1); - CountDownLatch releaseActiveMutation = new CountDownLatch(1); - CountDownLatch preCutoffMutationStarted = new CountDownLatch(1); - CountDownLatch releasePreCutoffMutation = new CountDownLatch(1); - CountDownLatch releasePostCutoffMutation = new CountDownLatch(1); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(2, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("pre-step-drain-stats"); - - lane.submitMutation("active mutation", () -> { - activeMutationStarted.countDown(); - assertTrue(releaseActiveMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(activeMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - lane.submitMutation("pre-cutoff mutation", () -> { - preCutoffMutationStarted.countDown(); - assertTrue(releasePreCutoffMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - - PhysicsOwnerStepCommand step = new PhysicsOwnerStepCommand( - new LegacyLiveHandleTestResource(), - 0.05f, - false, - 1L, - 1L); - assertTrue(lane.submitStepIfIdle(step)); - - lane.submitMutation("post-cutoff mutation", () -> { - assertTrue(releasePostCutoffMutation.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - - releaseActiveMutation.countDown(); - assertTrue(preCutoffMutationStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - releasePreCutoffMutation.countDown(); - PhysicsOwnerStepCompletion completedStep = pollStepCompletion(lane, - Duration.ofMillis(TIMEOUT_MILLIS)); - - assertNotNull(completedStep); - assertEquals(1, completedStep.preStepDrainedMutations()); - assertTrue(completedStep.preStepDrainRunNanos() > 0L); - assertEquals(1, completedStep.lateMutationBacklogAtStep()); - - releasePostCutoffMutation.countDown(); - assertEquals(2, pollMutationCompletions(lane, 2).size()); - } finally { - releaseActiveMutation.countDown(); - releasePreCutoffMutation.countDown(); - releasePostCutoffMutation.countDown(); - } - } - - @Test - void closedLaneRejectsNewWorkAndClosedSchedulerRejectsNewLanes() { - PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(1, - 2, - CLOSE_TIMEOUT); - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("close-reject"); - - lane.close(); - - assertTrue(lane.isClosed()); - assertFalse(lane.isStarted()); - assertThrows(RejectedExecutionException.class, - () -> lane.submitMutation("after close", PhysicsOwnerSnapshot::empty)); - assertThrows(RejectedExecutionException.class, - () -> lane.call("after close", () -> "rejected")); - - scheduler.close(); - assertThrows(RejectedExecutionException.class, scheduler::createLane); - } - - @Test - void nestedSameLaneOwnerCallsRunInlineWithoutQueuing() throws Exception { - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(1, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - List order = Collections.synchronizedList(new ArrayList<>()); - lane.start("nested-inline"); - - PhysicsMutationHandle outer = lane.enqueue("outer", null, () -> { - assertTrue(lane.isOwnerContext()); - order.add("outer-before"); - lane.run("nested run", () -> order.add("nested-run")); - String nestedValue = lane.call("nested call", () -> { - order.add("nested-call"); - return "done"; - }); - order.add(nestedValue); - }); - - awaitHandle(outer); - - assertEquals(List.of("outer-before", "nested-run", "nested-call", "done"), order); - assertEquals(1, pollMutationCompletions(lane, 1).size()); - } - } - - @Test - void completedMutationFutureIsNotReportedAsPendingCommand() throws Exception { - CountDownLatch commandStarted = new CountDownLatch(1); - CountDownLatch releaseCommand = new CountDownLatch(1); - CountDownLatch completionObserved = new CountDownLatch(1); - AtomicInteger pendingAtCompletion = new AtomicInteger(-1); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(1, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("completion-pending"); - - PhysicsMutationHandle handle = lane.submitMutation("blocked", () -> { - commandStarted.countDown(); - assertTrue(releaseCommand.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(commandStarted.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - handle.completion().whenComplete((ignored, failure) -> { - pendingAtCompletion.set(lane.pendingCommands()); - completionObserved.countDown(); - }); - - releaseCommand.countDown(); - assertTrue(completionObserved.await(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)); - - assertEquals(0, pendingAtCompletion.get()); - } finally { - releaseCommand.countDown(); - } - } - - @Test - void synchronousOwnerCallFromAnotherLaneContextIsRejected() throws Exception { - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(2, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource laneOne = scheduler.createLane(); - PhysicsOwnerLaneResource laneTwo = scheduler.createLane(); - AtomicBoolean crossLaneCallRan = new AtomicBoolean(); - laneOne.start("lane-one"); - laneTwo.start("lane-two"); - - PhysicsMutationHandle outer = laneOne.enqueue("outer", null, () -> { - assertTrue(laneOne.isOwnerContext()); - assertThrows(RejectedExecutionException.class, - () -> laneTwo.call("cross-lane call", () -> { - crossLaneCallRan.set(true); - return "not allowed"; - })); - }); - - awaitHandle(outer); - - assertFalse(crossLaneCallRan.get()); - assertEquals(1, pollMutationCompletions(laneOne, 1).size()); - assertTrue(laneTwo.pollCompletedMutations(1).isEmpty()); - } - } - - @Test - void pollCompletedMutationsReturnsBoundedFifoBatches() throws Exception { - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(2, - 8, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("bounded-poll"); - - PhysicsMutationHandle first = lane.submitMutation("first", - PhysicsOwnerSnapshot::empty); - PhysicsMutationHandle second = lane.submitMutation("second", - PhysicsOwnerSnapshot::empty); - PhysicsMutationHandle third = lane.submitMutation("third", - PhysicsOwnerSnapshot::empty); - awaitHandle(first); - awaitHandle(second); - awaitHandle(third); - - List firstBatch = lane.pollCompletedMutations(2); - List secondBatch = lane.pollCompletedMutations(2); - - assertEquals(List.of("first", "second"), operations(firstBatch)); - assertEquals(List.of("third"), operations(secondBatch)); - assertTrue(lane.pollCompletedMutations(2).isEmpty()); - } - } - - @Test - void unpolledMutationCompletionsApplyQueueBackpressure() throws Exception { - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(1, - 2, - CLOSE_TIMEOUT)) { - PhysicsOwnerLaneResource lane = scheduler.createLane(); - lane.start("pending-completion-backpressure"); - - PhysicsMutationHandle first = lane.submitMutation("first", - PhysicsOwnerSnapshot::empty); - PhysicsMutationHandle second = lane.submitMutation("second", - PhysicsOwnerSnapshot::empty); - awaitHandle(first); - awaitHandle(second); - - assertThrows(RejectedExecutionException.class, - () -> lane.submitMutation("third", PhysicsOwnerSnapshot::empty)); - - assertEquals(2, pollMutationCompletions(lane, 2).size()); - PhysicsMutationHandle third = lane.submitMutation("third", - PhysicsOwnerSnapshot::empty); - awaitHandle(third); - assertEquals(1, pollMutationCompletions(lane, 1).size()); - } - } - - private static List pollMutationCompletions( - @Nonnull PhysicsOwnerLaneResource lane, - int expected) throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(TIMEOUT_MILLIS); - List completions = new ArrayList<>(expected); - while (System.nanoTime() < deadline) { - completions.addAll(lane.pollCompletedMutations(expected - completions.size())); - if (completions.size() == expected) { - return completions; - } - Thread.sleep(10L); - } - return completions; - } - - private static void awaitHandle(@Nonnull PhysicsMutationHandle handle) - throws InterruptedException, ExecutionException, TimeoutException { - CompletableFuture future = handle.completion().toCompletableFuture(); - future.get(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); - } - - private static List operations( - @Nonnull List completions) { - return completions.stream() - .map(PhysicsOwnerMutationCompletion::operation) - .toList(); - } - - private static PhysicsOwnerStepCompletion pollStepCompletion( - @Nonnull PhysicsOwnerLaneResource lane, - @Nonnull Duration timeout) throws InterruptedException { - long deadline = System.nanoTime() + timeout.toNanos(); - PhysicsOwnerStepCompletion completion = null; - while (System.nanoTime() < deadline) { - completion = lane.pollCompletedStep(); - if (completion != null) { - return completion; - } - Thread.sleep(10L); - } - return lane.pollCompletedStep(); - } - - private static final class RecordingStepResource extends LegacyLiveHandleTestResource { - - @Nonnull - private final List order; - - private RecordingStepResource(@Nonnull List order) { - this.order = order; - } - - @Nonnull - @Override - public PublishedPhysicsSnapshotFrame capturePublishedSnapshotFrame(long stepSequence, - long serverTick, - @Nonnull PublishedPhysicsSnapshotFrame.Status status, - long stepNanos, - boolean profilingEnabled, - @Nonnull List physicsEvents, - int droppedBackendEventCount) { - order.add("step"); - return super.capturePublishedSnapshotFrame(stepSequence, - serverTick, - status, - stepNanos, - profilingEnabled, - physicsEvents, - droppedBackendEventCount); - } - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerStepCommandTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerStepCommandTest.java deleted file mode 100644 index 5607069c..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsOwnerStepCommandTest.java +++ /dev/null @@ -1,981 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBackendEventSink; -import dev.hytalemodding.impulse.api.PhysicsBackend; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsContact; -import dev.hytalemodding.impulse.api.PhysicsContactPhase; -import dev.hytalemodding.impulse.api.PhysicsJoint; -import dev.hytalemodding.impulse.api.PhysicsRayHit; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.capability.PhysicsCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsContinuousCollisionCapability; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsContactEvent; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsFrameEvent; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsEventCollectionMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.checkerframework.checker.nullness.compatqual.NonNullDecl; -import org.joml.Quaternionf; -import org.joml.Vector3f; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -class PhysicsOwnerStepCommandTest { - - private static final AtomicInteger BACKEND_COUNTER = new AtomicInteger(); - - @Test - void runsStepOnOwnerLaneAndPublishesProfiledSnapshot() throws Exception { - CountingBackend backend = registerBackend(true); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - CountingSpace space = (CountingSpace) resource.createLiveSpace(backend.getId(), - "owner-test", - PhysicsSpaceSettings.defaults()); - configureWorldSettings(resource, settings -> { - settings.setStepMode(PhysicsStepMode.FIXED); - settings.setSimulationSteps(2); - }); - - PhysicsBody first = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody second = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - second.setPosition(32.0f, 0.0f, 0.0f); - resource.addBody(space.id(), - first, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - resource.addBody(space.id(), - second, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(2, Duration.ofSeconds(2L))) { - owner.start("step-command-test"); - PhysicsOwnerStepCommand command = new PhysicsOwnerStepCommand(resource, - 0.05f, - true, - 12L, - 34L); - PhysicsOwnerResult result = owner.submitAndDrain(command); - - assertEquals(1, result.snapshot().spaces()); - assertEquals(2, result.snapshot().substeps()); - assertEquals(2, result.snapshot().bodySnapshots()); - assertEquals(2, result.snapshot().spatialIndexCells()); - assertTrue(result.snapshot().stepNanos() > 0L); - assertTrue(result.snapshot().snapshotNanos() > 0L); - PublishedPhysicsSnapshotFrame frame = command.publishedFrame(); - Assertions.assertNotNull(frame); - assertEquals(PublishedPhysicsSnapshotFrame.Status.COMPLETE, frame.status()); - assertEquals(12L, frame.stepSequence()); - assertEquals(34L, frame.serverTick()); - assertEquals(2, frame.bodyCount()); - assertEquals(2, space.stepThreadNames.size()); - assertTrue(space.stepThreadNames.stream() - .allMatch(name -> name.startsWith("Impulse physics owner lane executor "))); - assertEquals(List.of(0.025f, 0.025f), space.stepDts); - } - } - - @Test - void progressiveRefinementUsesMaxStepDtBudget() { - CountingBackend backend = registerBackend(false); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - CountingSpace space = (CountingSpace) resource.createLiveSpace(backend.getId(), - "owner-test", - PhysicsSpaceSettings.defaults()); - configureWorldSettings(resource, settings -> { - settings.setStepMode(PhysicsStepMode.PROGRESSIVE_REFINEMENT); - settings.setSimulationSteps(2); - settings.setMaxStepDt(0.1f); - }); - - PhysicsOwnerSnapshot snapshot = PhysicsOwnerStepCommand.runStep(resource, 0.35f, false); - - assertEquals(1, snapshot.spaces()); - assertEquals(4, snapshot.substeps()); - assertEquals(List.of(0.0875f, 0.0875f, 0.0875f, 0.0875f), space.stepDts); - assertEquals(0L, snapshot.stepNanos()); - assertEquals(0L, snapshot.snapshotNanos()); - } - - @Test - void nonFiniteDtDoesNotReachBackendStep() { - CountingBackend backend = registerBackend(false); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - CountingSpace space = (CountingSpace) resource.createLiveSpace(backend.getId(), - "owner-test", - PhysicsSpaceSettings.defaults()); - configureWorldSettings(resource, settings -> settings.setStepMode(PhysicsStepMode.FIXED)); - - PhysicsOwnerStepCommand.runStep(resource, Float.NaN, false); - PhysicsOwnerStepCommand.runStep(resource, Float.POSITIVE_INFINITY, false); - PhysicsOwnerStepCommand.runStep(resource, -1.0f, false); - - assertEquals(List.of(0.0f, 0.0f, 0.0f), space.stepDts); - } - - @Test - void adaptiveRefinementRaisesStepsForFastBodies() { - CountingBackend backend = registerBackend(false); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - CountingSpace space = (CountingSpace) resource.createLiveSpace(backend.getId(), - "owner-test", - PhysicsSpaceSettings.defaults()); - configureWorldSettings(resource, settings -> { - settings.setStepMode(PhysicsStepMode.ADAPTIVE); - settings.setSimulationSteps(1); - settings.setMaxStepDt(1.0f); - }); - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - body.setLinearVelocity(2.0f, 0.0f, 0.0f); - resource.addBody(space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - - PhysicsOwnerSnapshot snapshot = PhysicsOwnerStepCommand.runStep(resource, 0.5f, false); - - assertEquals(3, snapshot.substeps()); - assertEquals(3, space.stepDts.size()); - assertEquals(1, snapshot.bodySnapshots()); - } - - @Test - void disabledEventCollectionStepsWithoutBackendEventSink() { - CountingBackend backend = registerBackend(false); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - CountingSpace space = (CountingSpace) resource.createLiveSpace(backend.getId(), - "owner-test", - PhysicsSpaceSettings.defaults()); - configureWorldSettings(resource, settings -> { - settings.setStepMode(PhysicsStepMode.FIXED); - settings.setSimulationSteps(1); - }); - PhysicsBody first = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody second = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - resource.addBody(space.id(), - first, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - resource.addBody(space.id(), - second, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - space.contactToEmit = new PhysicsContact(first, - second, - new Vector3f(1.0f, 2.0f, 3.0f), - new Vector3f(4.0f, 5.0f, 6.0f), - new Vector3f(0.0f, 1.0f, 0.0f), - -0.125f, - 2.5f); - - PhysicsOwnerStepCommand.runStep(resource, 0.05f, false); - - PhysicsEventFrame frame = resource.getLatestEventFrame(); - assertEquals(0, space.eventStepCount); - assertEquals(0, frame.physicsEventCount()); - } - - @Test - void translatesBackendContactEventsToStableBodyKeysInStepFrame() { - CountingBackend backend = registerBackend(false); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - CountingSpace space = (CountingSpace) resource.createLiveSpace(backend.getId(), - "owner-test", - PhysicsSpaceSettings.defaults()); - configureWorldSettings(resource, settings -> { - settings.setStepMode(PhysicsStepMode.FIXED); - settings.setSimulationSteps(1); - settings.setEventCollectionMode(PhysicsEventCollectionMode.CONTACTS); - }); - PhysicsBody first = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody second = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - RigidBodyKey firstKey = resource.addBody(space.id(), - first, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - RigidBodyKey secondKey = resource.addBody(space.id(), - second, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - space.contactToEmit = new PhysicsContact(first, - second, - new Vector3f(1.0f, 2.0f, 3.0f), - new Vector3f(4.0f, 5.0f, 6.0f), - new Vector3f(0.0f, 1.0f, 0.0f), - -0.125f, - 2.5f); - - PhysicsOwnerStepCommand.runStep(resource, 0.05f, false); - - PhysicsEventFrame frame = resource.getLatestEventFrame(); - assertEquals(1, frame.physicsEventCount()); - PhysicsContactEvent event = - assertInstanceOf(PhysicsContactEvent.class, frame.physicsEvents().getFirst()); - assertEquals(space.id(), event.spaceId()); - assertEquals(PhysicsContactPhase.OBSERVED, event.phase()); - assertEquals(firstKey, event.bodyAKey()); - assertEquals(secondKey, event.bodyBKey()); - assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), event.pointOnA()); - assertEquals(new Vector3f(4.0f, 5.0f, 6.0f), event.pointOnB()); - assertEquals(new Vector3f(0.0f, 1.0f, 0.0f), event.normalOnB()); - assertEquals(-0.125f, event.distance()); - assertEquals(2.5f, event.impulse()); - } - - @Test - void ccdModeForcesAndRestoresOnlyOwnerAppliedOverrides() { - CountingBackend backend = registerBackend(true); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - CountingSpace space = (CountingSpace) resource.createLiveSpace(backend.getId(), - "owner-test", - PhysicsSpaceSettings.defaults()); - PhysicsBody forced = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody alreadyEnabled = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody unregistered = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - space.addBody(unregistered); - alreadyEnabled.setContinuousCollisionEnabled(true); - resource.addBody(space.id(), - forced, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - resource.addBody(space.id(), - alreadyEnabled, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - - configureWorldSettings(resource, settings -> settings.setStepMode(PhysicsStepMode.CCD)); - PhysicsOwnerStepCommand.runStep(resource, 0.05f, false); - - assertTrue(forced.isContinuousCollisionEnabled()); - assertTrue(alreadyEnabled.isContinuousCollisionEnabled()); - assertFalse(unregistered.isContinuousCollisionEnabled()); - - configureWorldSettings(resource, settings -> settings.setStepMode(PhysicsStepMode.FIXED)); - PhysicsOwnerStepCommand.runStep(resource, 0.05f, false); - - assertFalse(forced.isContinuousCollisionEnabled()); - assertTrue(alreadyEnabled.isContinuousCollisionEnabled()); - assertFalse(unregistered.isContinuousCollisionEnabled()); - } - - @Test - void stepFailuresPublishSnapshotsAndRemainInspectable() throws Exception { - CountingBackend backend = registerBackend(false); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - CountingSpace space = (CountingSpace) resource.createLiveSpace(backend.getId(), - "owner-test", - PhysicsSpaceSettings.defaults()); - RuntimeException failure = new RuntimeException("step failed"); - space.stepFailure = failure; - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(1, - 1, - Duration.ofSeconds(2L))) { - owner.start("failing-step-command-test"); - PhysicsOwnerStepCommand command = new PhysicsOwnerStepCommand(resource, - 0.05f, - false); - PhysicsOwnerResult result = owner.submitAndDrain(command); - - assertSame(failure, command.failure()); - Assertions.assertNotNull(command.publishedFrame()); - assertEquals(PublishedPhysicsSnapshotFrame.Status.PARTIAL, - command.publishedFrame().status()); - assertEquals(1, result.snapshot().spaces()); - assertEquals(0, result.snapshot().substeps()); - assertEquals(0, owner.pendingCommands()); - } - } - - @Test - void snapshotFailureIsSuppressedWhenStepAlreadyFailed() { - CountingBackend backend = registerBackend(false); - FailingSnapshotWorldResource resource = new FailingSnapshotWorldResource(); - CountingSpace space = (CountingSpace) resource.createLiveSpace(backend.getId(), - "owner-test", - PhysicsSpaceSettings.defaults()); - RuntimeException stepFailure = new RuntimeException("step failed"); - RuntimeException snapshotFailure = new RuntimeException("snapshot failed"); - space.stepFailure = stepFailure; - resource.snapshotFailure = snapshotFailure; - - RuntimeException thrown = assertThrows(RuntimeException.class, - () -> PhysicsOwnerStepCommand.runStep(resource, 0.05f, false)); - - assertSame(stepFailure, thrown); - assertEquals(1, thrown.getSuppressed().length); - assertSame(snapshotFailure, thrown.getSuppressed()[0]); - } - - @Nonnull - private static CountingBackend registerBackend(boolean supportsContinuousCollision) { - CountingBackend backend = new CountingBackend("test:owner-step-" - + BACKEND_COUNTER.incrementAndGet(), supportsContinuousCollision); - Impulse.registerBackend(backend); - return backend; - } - - private static void configureWorldSettings(@Nonnull LegacyLiveHandleTestResource resource, - @Nonnull Consumer configurator) { - PhysicsWorldSettings settings = resource.getWorldSettings(); - configurator.accept(settings); - resource.setWorldSettings(settings); - } - - private static final class CountingBackend implements PhysicsBackend { - - @Nonnull - private final BackendId id; - private final boolean supportsContinuousCollision; - - private CountingBackend(@Nonnull String id, boolean supportsContinuousCollision) { - this.id = new BackendId(id); - this.supportsContinuousCollision = supportsContinuousCollision; - } - - @Nonnull - @Override - public BackendId getId() { - return id; - } - - @Override - public void init() { - } - - @Nonnull - @Override - public PhysicsSpace createSpace() { - return createSpace(SpaceId.next()); - } - - @Nonnull - @Override - public PhysicsSpace createSpace(@Nonnull SpaceId spaceId) { - return new CountingSpace(spaceId, id, supportsContinuousCollision); - } - } - - private static final class FailingSnapshotWorldResource extends LegacyLiveHandleTestResource { - - @Nullable - private RuntimeException snapshotFailure; - - @NonNullDecl - @Override - public PublishedPhysicsSnapshotFrame capturePublishedSnapshotFrame(long stepSequence, - long serverTick, - @NonNullDecl PublishedPhysicsSnapshotFrame.Status status, - long stepNanos, - boolean profilingEnabled, - @NonNullDecl List physicsEvents, - int droppedBackendEventCount) { - if (snapshotFailure != null) { - throw snapshotFailure; - } - return super.capturePublishedSnapshotFrame(stepSequence, - serverTick, - status, - stepNanos, - profilingEnabled, - physicsEvents, - droppedBackendEventCount); - } - } - - private static final class CountingSpace implements PhysicsSpace { - - @Nonnull - private final SpaceId id; - @Nonnull - private final BackendId backendId; - private final boolean supportsContinuousCollision; - @Nonnull - private final List bodies = new ArrayList<>(); - @Nonnull - private final Vector3f gravity = new Vector3f(); - @Nonnull - private final List stepDts = new ArrayList<>(); - @Nonnull - private final List stepThreadNames = new ArrayList<>(); - @Nullable - private RuntimeException stepFailure; - @Nullable - private PhysicsContact contactToEmit; - private int eventStepCount; - - private CountingSpace(@Nonnull SpaceId id, - @Nonnull BackendId backendId, - boolean supportsContinuousCollision) { - this.id = id; - this.backendId = backendId; - this.supportsContinuousCollision = supportsContinuousCollision; - } - - @Nonnull - @Override - public SpaceId id() { - return id; - } - - @Nonnull - @Override - public BackendId backendId() { - return backendId; - } - - @Override - public void step(float dt) { - if (stepFailure != null) { - throw stepFailure; - } - stepDts.add(dt); - stepThreadNames.add(Thread.currentThread().getName()); - } - - @Override - public void step(float dt, @Nonnull PhysicsBackendEventSink events) { - eventStepCount++; - step(dt); - if (contactToEmit != null) { - events.contact(PhysicsContactPhase.OBSERVED, - contactToEmit.bodyA(), - contactToEmit.bodyB(), - contactToEmit.pointOnA(), - contactToEmit.pointOnB(), - contactToEmit.normalOnB(), - contactToEmit.distance(), - contactToEmit.impulse()); - } - } - - @Override - public void setGravity(float x, float y, float z) { - gravity.set(x, y, z); - } - - @Nonnull - @Override - public Vector3f getGravity() { - return new Vector3f(gravity); - } - - @Override - public void addBody(@Nonnull PhysicsBody body) { - bodies.add(body); - } - - @Override - public void removeBody(@Nonnull PhysicsBody body) { - bodies.remove(body); - } - - @Nonnull - @Override - public List getBodies() { - return new ArrayList<>(bodies); - } - - @Override - public int bodyCount() { - return bodies.size(); - } - - @Override - public boolean containsBody(@Nonnull PhysicsBody body) { - return bodies.contains(body); - } - - @NonNullDecl - @Override - public Optional getCapability(@Nonnull Class type) { - Objects.requireNonNull(type, "type"); - if (supportsContinuousCollision && type == PhysicsContinuousCollisionCapability.class) { - return Optional.of(type.cast(new PhysicsContinuousCollisionCapability() { - })); - } - return Optional.empty(); - } - - @Nonnull - @Override - public PhysicsBody createStaticPlane(float groundY) { - CountingBody body = new CountingBody(ShapeType.PLANE, PhysicsBodyType.STATIC); - body.position.y = groundY; - return body; - } - - @Nonnull - @Override - public PhysicsBody createBox(float halfX, float halfY, float halfZ, float mass) { - CountingBody body = new CountingBody(ShapeType.BOX, PhysicsBodyType.DYNAMIC); - body.halfExtents.set(halfX, halfY, halfZ); - body.mass = mass; - return body; - } - - @Nonnull - @Override - public PhysicsBody createBox(@Nonnull Vector3f halfExtents, float mass) { - return createBox(halfExtents.x, halfExtents.y, halfExtents.z, mass); - } - - @Nonnull - @Override - public PhysicsBody createSphere(float radius, float mass) { - CountingBody body = new CountingBody(ShapeType.SPHERE, PhysicsBodyType.DYNAMIC); - body.radius = radius; - body.mass = mass; - return body; - } - - @Nonnull - @Override - public PhysicsBody createCapsule(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - return createRoundHeightBody(ShapeType.CAPSULE, radius, halfHeight, axis, mass); - } - - @Nonnull - @Override - public PhysicsBody createCylinder(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - return createRoundHeightBody(ShapeType.CYLINDER, radius, halfHeight, axis, mass); - } - - @Nonnull - @Override - public PhysicsBody createCone(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - return createRoundHeightBody(ShapeType.CONE, radius, halfHeight, axis, mass); - } - - @Nonnull - @Override - public Optional raycastClosest(@Nonnull Vector3f from, @Nonnull Vector3f to) { - return Optional.empty(); - } - - @Nonnull - @Override - public List raycastAll(@Nonnull Vector3f from, @Nonnull Vector3f to) { - return List.of(); - } - - @Nonnull - @Override - public List getContacts() { - return contactToEmit != null ? List.of(contactToEmit) : List.of(); - } - - @Nonnull - @Override - public PhysicsJoint createFixedJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB) { - throw new UnsupportedOperationException(); - } - - @Nonnull - @Override - public PhysicsJoint createPointJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB) { - throw new UnsupportedOperationException(); - } - - @Nonnull - @Override - public PhysicsJoint createHingeJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis) { - throw new UnsupportedOperationException(); - } - - @Nonnull - @Override - public PhysicsJoint createSliderJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis) { - throw new UnsupportedOperationException(); - } - - @Nonnull - @Override - public PhysicsJoint createSpringJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - float restLength, - float stiffness, - float damping) { - throw new UnsupportedOperationException(); - } - - @Override - public void removeJoint(@Nonnull PhysicsJoint joint) { - } - - @Nonnull - @Override - public List getJoints() { - return List.of(); - } - - @Nonnull - private PhysicsBody createRoundHeightBody(@Nonnull ShapeType shapeType, - float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - CountingBody body = new CountingBody(shapeType, PhysicsBodyType.DYNAMIC); - body.radius = radius; - body.halfHeight = halfHeight; - body.axis = axis; - body.mass = mass; - return body; - } - } - - private static final class CountingBody implements PhysicsBody { - - @Nonnull - private final ShapeType shapeType; - @Nonnull - private final Vector3f position = new Vector3f(); - @Nonnull - private final Quaternionf rotation = new Quaternionf(); - @Nonnull - private final Vector3f linearVelocity = new Vector3f(); - @Nonnull - private final Vector3f angularVelocity = new Vector3f(); - @Nonnull - private final Vector3f halfExtents = new Vector3f(); - @Nonnull - private PhysicsBodyType bodyType; - @Nonnull - private PhysicsAxis axis = PhysicsAxis.Y; - private float mass = 1.0f; - private float radius; - private float halfHeight; - private float restitution; - private float friction; - private float linearDamping; - private float angularDamping; - private boolean sensor; - private boolean continuousCollision; - private int collisionGroup; - private int collisionMask; - - private CountingBody(@Nonnull ShapeType shapeType, @Nonnull PhysicsBodyType bodyType) { - this.shapeType = shapeType; - this.bodyType = bodyType; - } - - @Override - public void setPosition(float x, float y, float z) { - position.set(x, y, z); - } - - @Override - public void setPosition(@Nonnull Vector3f pos) { - position.set(pos); - } - - @Nonnull - @Override - public Vector3f getPosition() { - return new Vector3f(position); - } - - @Override - public void setRotation(float x, float y, float z, float w) { - rotation.set(x, y, z, w); - } - - @Override - public void setRotation(@Nonnull Quaternionf rot) { - rotation.set(rot); - } - - @Nonnull - @Override - public Quaternionf getRotation() { - return new Quaternionf(rotation); - } - - @Override - public void setRestitution(float restitution) { - this.restitution = restitution; - } - - @Override - public float getRestitution() { - return restitution; - } - - @Override - public void setFriction(float friction) { - this.friction = friction; - } - - @Override - public float getFriction() { - return friction; - } - - @Nonnull - @Override - public PhysicsBodyType getBodyType() { - return bodyType; - } - - @Override - public void setBodyType(@Nonnull PhysicsBodyType bodyType) { - this.bodyType = bodyType; - } - - @Override - public boolean isStatic() { - return bodyType == PhysicsBodyType.STATIC; - } - - @Override - public boolean isKinematic() { - return bodyType == PhysicsBodyType.KINEMATIC; - } - - @Override - public void setKinematic(boolean kinematic) { - bodyType = kinematic ? PhysicsBodyType.KINEMATIC : PhysicsBodyType.DYNAMIC; - } - - @Override - public void activate() { - } - - @Override - public boolean isActive() { - return true; - } - - @Override - public boolean isSleeping() { - return false; - } - - @Override - public void sleep() { - } - - @Override - public float getMass() { - return mass; - } - - @Override - public void setMass(float mass) { - this.mass = mass; - } - - @Nonnull - @Override - public Vector3f getLinearVelocity() { - return new Vector3f(linearVelocity); - } - - @Override - public void setLinearVelocity(@Nonnull Vector3f vel) { - linearVelocity.set(vel); - } - - @Override - public void setLinearVelocity(float x, float y, float z) { - linearVelocity.set(x, y, z); - } - - @Nonnull - @Override - public Vector3f getAngularVelocity() { - return new Vector3f(angularVelocity); - } - - @Override - public void setAngularVelocity(@Nonnull Vector3f vel) { - angularVelocity.set(vel); - } - - @Override - public void setAngularVelocity(float x, float y, float z) { - angularVelocity.set(x, y, z); - } - - @Override - public float getLinearDamping() { - return linearDamping; - } - - @Override - public void setLinearDamping(float damping) { - linearDamping = damping; - } - - @Override - public float getAngularDamping() { - return angularDamping; - } - - @Override - public void setAngularDamping(float damping) { - angularDamping = damping; - } - - @Override - public void applyCentralForce(@Nonnull Vector3f force) { - } - - @Override - public void applyCentralForce(float x, float y, float z) { - } - - @Override - public void applyForce(@Nonnull Vector3f force, @Nonnull Vector3f offset) { - } - - @Override - public void applyCentralImpulse(@Nonnull Vector3f impulse) { - } - - @Override - public void applyCentralImpulse(float x, float y, float z) { - } - - @Override - public void applyImpulse(@Nonnull Vector3f impulse, @Nonnull Vector3f offset) { - } - - @Override - public void applyTorque(@Nonnull Vector3f torque) { - } - - @Override - public void applyTorqueImpulse(@Nonnull Vector3f torqueImpulse) { - } - - @Override - public void clearForces() { - } - - @Override - public boolean isSensor() { - return sensor; - } - - @Override - public void setSensor(boolean sensor) { - this.sensor = sensor; - } - - @Override - public int getCollisionGroup() { - return collisionGroup; - } - - @Override - public int getCollisionMask() { - return collisionMask; - } - - @Override - public void setCollisionFilter(int group, int mask) { - collisionGroup = group; - collisionMask = mask; - } - - @Override - public boolean isContinuousCollisionEnabled() { - return continuousCollision; - } - - @Override - public void setContinuousCollisionEnabled(boolean enabled) { - continuousCollision = enabled; - } - - @Nonnull - @Override - public ShapeType getShapeType() { - return shapeType; - } - - @Nonnull - @Override - public Vector3f getBoxHalfExtents() { - return new Vector3f(halfExtents); - } - - @Override - public float getSphereRadius() { - return radius; - } - - @Override - public float getHalfHeight() { - return halfHeight; - } - - @Nonnull - @Override - public PhysicsAxis getShapeAxis() { - return axis; - } - - @Override - public float getCenterOfMassOffsetY() { - return 0.0f; - } - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsWorldResourceCommandVisibilityTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsWorldResourceCommandVisibilityTest.java deleted file mode 100644 index 748a94dd..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/PhysicsWorldResourceCommandVisibilityTest.java +++ /dev/null @@ -1,519 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandResult; -import dev.hytalemodding.impulse.core.plugin.simulation.query.SpaceBodyCountQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import java.time.Duration; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import javax.annotation.Nonnull; -import org.junit.jupiter.api.Test; - -class PhysicsWorldResourceCommandVisibilityTest { - - private static final AtomicInteger BACKEND_COUNTER = new AtomicInteger(); - - @Test - void ownerCommandSpawnPublishesRegistrationViewsWithSnapshotFrame() throws Exception { - RuntimeFixture fixture = createRuntimeFixture("owner-command-registration-view"); - PhysicsWorldRuntimeResource resource = fixture.resource(); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("owner-command-registration-view"); - resource.attachOwnerExecutor(owner); - - CountDownLatch blockerStarted = new CountDownLatch(1); - CountDownLatch releaseBlocker = new CountDownLatch(1); - owner.submitMutation("block command-buffer spawn", () -> { - blockerStarted.countDown(); - assertTrue(releaseBlocker.await(2, TimeUnit.SECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(blockerStarted.await(2, TimeUnit.SECONDS)); - - RigidBodyKey bodyId = RigidBodyKey.random(); - var handle = resource.submitCommands(201L, commands -> commands - .spawnBody(bodyId, spawn -> spawn - .space(fixture.spaceId()) - .box(0.5f, 0.5f, 0.5f) - .dynamic())); - - assertFalse(handle.completion().toCompletableFuture().isDone()); - assertNull(resource.getBodyRegistrationView(bodyId)); - assertEquals(0, resource.getBodyRegistrationCount()); - - releaseBlocker.countDown(); - var results = handle.completion() - .toCompletableFuture() - .get(2, TimeUnit.SECONDS); - - assertEquals(PhysicsCommandResult.Status.APPLIED, results.getFirst().status()); - assertNull(resource.getBodyRegistrationView(bodyId)); - assertEquals(0, resource.getBodyRegistrationCount()); - assertTrue(resource.isBodyCreationPending(bodyId)); - assertFalse(resource.isBodyCreationPending(RigidBodyKey.random())); - - PublishedPhysicsSnapshotFrame frame = resource.capturePublishedSnapshotFrame(1L, - 202L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - - assertEquals(1, frame.bodyCount()); - assertNull(resource.getBodyRegistrationView(bodyId)); - assertEquals(0, resource.getBodyRegistrationCount()); - - resource.applyPublishedSnapshotFrame(frame); - - assertNotNull(resource.getBodyRegistrationView(bodyId)); - assertEquals(1, resource.getBodyRegistrationCount()); - assertFalse(resource.isBodyCreationPending(bodyId)); - resource.detachOwnerExecutor(owner); - } - } - - @Test - void multiSingleSpawnCommandTracksOnlySpawnedPendingKeys() throws Exception { - RuntimeFixture fixture = createRuntimeFixture("owner-command-registration-multi-single"); - PhysicsWorldRuntimeResource resource = fixture.resource(); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("owner-command-registration-multi-single"); - resource.attachOwnerExecutor(owner); - - RigidBodyKey firstBodyId = RigidBodyKey.random(); - RigidBodyKey secondBodyId = RigidBodyKey.random(); - var handle = resource.submitCommands(301L, commands -> { - commands.spawnBody(firstBodyId, spawn -> spawn - .space(fixture.spaceId()) - .box(0.5f, 0.5f, 0.5f) - .dynamic()); - commands.spawnBody(secondBodyId, spawn -> spawn - .space(fixture.spaceId()) - .box(0.5f, 0.5f, 0.5f) - .dynamic()); - }); - - handle.completion().toCompletableFuture().get(2, TimeUnit.SECONDS); - - assertTrue(resource.isBodyCreationPending(firstBodyId)); - assertTrue(resource.isBodyCreationPending(secondBodyId)); - assertTrue(resource.hasPublishedOrPendingBodyRegistration(firstBodyId)); - assertTrue(resource.hasPublishedOrPendingBodyRegistration(secondBodyId)); - assertFalse(resource.isBodyCreationPending(RigidBodyKey.random())); - assertFalse(resource.hasPublishedOrPendingBodyRegistration(RigidBodyKey.random())); - - PublishedPhysicsSnapshotFrame frame = resource.capturePublishedSnapshotFrame(1L, - 302L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - resource.applyPublishedSnapshotFrame(frame); - - assertFalse(resource.isBodyCreationPending(firstBodyId)); - assertFalse(resource.isBodyCreationPending(secondBodyId)); - resource.detachOwnerExecutor(owner); - } - } - - @Test - void commandSpawnedBodyIsIndexedForInternalReadersBeforePublishedRegistration() throws Exception { - RuntimeFixture fixture = createRuntimeFixture("owner-command-indexed-snapshot"); - PhysicsWorldRuntimeResource resource = fixture.resource(); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("owner-command-indexed-snapshot"); - resource.attachOwnerExecutor(owner); - - RigidBodyKey bodyId = RigidBodyKey.random(); - var handle = resource.submitCommands(306L, commands -> commands - .spawnBody(bodyId, spawn -> spawn - .space(fixture.spaceId()) - .box(0.5f, 0.5f, 0.5f) - .dynamic() - .kind(PhysicsBodyKind.BODY) - .runtimeOnly())); - - handle.completion().toCompletableFuture().get(2, TimeUnit.SECONDS); - - assertNull(resource.getBodyRegistrationView(bodyId)); - assertTrue(resource.isBodyCreationPending(bodyId)); - - AtomicInteger indexedSnapshots = new AtomicInteger(); - AtomicBoolean foundBody = new AtomicBoolean(); - resource.callOwner("inspect command-spawned indexed body snapshot", () -> { - resource.forEachIndexedBodySnapshot(fixture.spaceId(), - (snapshotBodyId, snapshot, snapshotSpaceId, kind, persistenceMode) -> { - indexedSnapshots.incrementAndGet(); - if (bodyId.equals(snapshotBodyId)) { - foundBody.set(true); - assertEquals(fixture.spaceId(), snapshotSpaceId); - assertTrue(snapshot.isDynamic()); - assertEquals(PhysicsBodyKind.BODY, kind); - assertEquals(PhysicsBodyPersistenceMode.RUNTIME_ONLY, persistenceMode); - } - }); - return null; - }); - - assertEquals(1, indexedSnapshots.get()); - assertTrue(foundBody.get()); - resource.detachOwnerExecutor(owner); - } - } - - @Test - void bulkSpawnCommandTracksOnlySpawnedPendingKeys() throws Exception { - RuntimeFixture fixture = createRuntimeFixture("owner-command-registration-bulk"); - PhysicsWorldRuntimeResource resource = fixture.resource(); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("owner-command-registration-bulk"); - resource.attachOwnerExecutor(owner); - - RigidBodyKey firstBodyId = RigidBodyKey.random(); - RigidBodyKey secondBodyId = RigidBodyKey.random(); - var handle = resource.submitCommands(311L, commands -> commands.spawnBodies(2, spawns -> { - spawns.body(firstBodyId, spawn -> spawn - .space(fixture.spaceId()) - .box(0.5f, 0.5f, 0.5f) - .dynamic()); - spawns.body(secondBodyId, spawn -> spawn - .space(fixture.spaceId()) - .box(0.5f, 0.5f, 0.5f) - .dynamic()); - })); - - handle.completion().toCompletableFuture().get(2, TimeUnit.SECONDS); - - assertTrue(resource.isBodyCreationPending(firstBodyId)); - assertTrue(resource.isBodyCreationPending(secondBodyId)); - assertTrue(resource.hasPublishedOrPendingBodyRegistration(firstBodyId)); - assertTrue(resource.hasPublishedOrPendingBodyRegistration(secondBodyId)); - assertFalse(resource.isBodyCreationPending(RigidBodyKey.random())); - assertFalse(resource.hasPublishedOrPendingBodyRegistration(RigidBodyKey.random())); - - PublishedPhysicsSnapshotFrame frame = resource.capturePublishedSnapshotFrame(1L, - 312L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - resource.applyPublishedSnapshotFrame(frame); - - assertFalse(resource.isBodyCreationPending(firstBodyId)); - assertFalse(resource.isBodyCreationPending(secondBodyId)); - assertNotNull(resource.getBodyRegistrationView(firstBodyId)); - assertNotNull(resource.getBodyRegistrationView(secondBodyId)); - resource.detachOwnerExecutor(owner); - } - } - - @Test - void templateSpawnCommandTracksOnlySpawnedPendingKeys() throws Exception { - RuntimeFixture fixture = createRuntimeFixture("owner-command-registration-template"); - PhysicsWorldRuntimeResource resource = fixture.resource(); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("owner-command-registration-template"); - resource.attachOwnerExecutor(owner); - - RigidBodyKey firstBodyId = RigidBodyKey.random(); - RigidBodyKey secondBodyId = RigidBodyKey.random(); - var handle = resource.submitCommands(321L, commands -> commands.spawnBodies(2, - fixture.spaceId(), - PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f), - 1.0f, - PhysicsBodyType.DYNAMIC, - RigidBodySpawnSettings.defaults(), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY, - spawns -> spawns - .body(firstBodyId, 0.0f, 0.0f, 0.0f) - .body(secondBodyId, 1.0f, 0.0f, 0.0f))); - - handle.completion().toCompletableFuture().get(2, TimeUnit.SECONDS); - - assertTrue(resource.isBodyCreationPending(firstBodyId)); - assertTrue(resource.isBodyCreationPending(secondBodyId)); - assertTrue(resource.hasPublishedOrPendingBodyRegistration(firstBodyId)); - assertTrue(resource.hasPublishedOrPendingBodyRegistration(secondBodyId)); - assertFalse(resource.isBodyCreationPending(RigidBodyKey.random())); - assertFalse(resource.hasPublishedOrPendingBodyRegistration(RigidBodyKey.random())); - - PublishedPhysicsSnapshotFrame frame = resource.capturePublishedSnapshotFrame(1L, - 322L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - resource.applyPublishedSnapshotFrame(frame); - - assertFalse(resource.isBodyCreationPending(firstBodyId)); - assertFalse(resource.isBodyCreationPending(secondBodyId)); - assertNotNull(resource.getBodyRegistrationView(firstBodyId)); - assertNotNull(resource.getBodyRegistrationView(secondBodyId)); - resource.detachOwnerExecutor(owner); - } - } - - @Test - void rejectedSpawnCommandClearsPendingKeyAfterIncludedSnapshotFrame() throws Exception { - RuntimeFixture fixture = createRuntimeFixture("owner-command-registration-rejected"); - PhysicsWorldRuntimeResource resource = fixture.resource(); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("owner-command-registration-rejected"); - resource.attachOwnerExecutor(owner); - - RigidBodyKey bodyId = RigidBodyKey.random(); - var handle = resource.submitCommands(331L, commands -> commands - .spawnBody(bodyId, spawn -> spawn - .space(new SpaceId(Integer.MAX_VALUE)) - .box(0.5f, 0.5f, 0.5f) - .dynamic())); - - var results = handle.completion().toCompletableFuture().get(2, TimeUnit.SECONDS); - - assertEquals(PhysicsCommandResult.Status.REJECTED, results.getFirst().status()); - assertTrue(resource.isBodyCreationPending(bodyId)); - assertFalse(resource.isBodyCreationPending(RigidBodyKey.random())); - - PublishedPhysicsSnapshotFrame frame = resource.capturePublishedSnapshotFrame(1L, - 332L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - resource.applyPublishedSnapshotFrame(frame); - - assertFalse(resource.isBodyCreationPending(bodyId)); - assertNull(resource.getBodyRegistrationView(bodyId)); - resource.detachOwnerExecutor(owner); - } - } - - @Test - void partiallyRejectedBulkSpawnClearsAllRequestedPendingKeysAfterIncludedSnapshotFrame() throws Exception { - RuntimeFixture fixture = createRuntimeFixture("owner-command-registration-partial"); - PhysicsWorldRuntimeResource resource = fixture.resource(); - - RigidBodyKey createdBodyId = RigidBodyKey.random(); - RigidBodyKey duplicateBodyId = RigidBodyKey.random(); - spawnPublishedBody(resource, fixture.spaceId(), duplicateBodyId); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("owner-command-registration-partial"); - resource.attachOwnerExecutor(owner); - - var handle = resource.submitCommands(341L, commands -> commands.spawnBodies(2, spawns -> { - spawns.body(createdBodyId, spawn -> spawn - .space(fixture.spaceId()) - .box(0.5f, 0.5f, 0.5f) - .dynamic()); - spawns.body(duplicateBodyId, spawn -> spawn - .space(fixture.spaceId()) - .box(0.5f, 0.5f, 0.5f) - .dynamic()); - })); - - var results = handle.completion().toCompletableFuture().get(2, TimeUnit.SECONDS); - - assertEquals(PhysicsCommandResult.Status.REJECTED, results.getFirst().status()); - assertTrue(resource.isBodyCreationPending(createdBodyId)); - assertTrue(resource.isBodyCreationPending(duplicateBodyId)); - assertFalse(resource.isBodyCreationPending(RigidBodyKey.random())); - - PublishedPhysicsSnapshotFrame frame = resource.capturePublishedSnapshotFrame(1L, - 342L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - resource.applyPublishedSnapshotFrame(frame); - - assertNotNull(resource.getBodyRegistrationView(createdBodyId)); - assertNotNull(resource.getBodyRegistrationView(duplicateBodyId)); - assertFalse(resource.isBodyCreationPending(createdBodyId)); - assertFalse(resource.isBodyCreationPending(duplicateBodyId)); - resource.detachOwnerExecutor(owner); - } - } - - @Test - void nullableSnapshotLookupToleratesStalePublishedRegistrationAfterDestroy() throws Exception { - RuntimeFixture fixture = createRuntimeFixture("owner-command-stale-registration-snapshot"); - PhysicsWorldRuntimeResource resource = fixture.resource(); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("owner-command-stale-registration-snapshot"); - resource.attachOwnerExecutor(owner); - - RigidBodyKey bodyId = RigidBodyKey.random(); - spawnPublishedBody(resource, fixture.spaceId(), bodyId); - - resource.destroyBodyAsync(bodyId).completion() - .toCompletableFuture() - .get(2, TimeUnit.SECONDS); - - assertNotNull(resource.getBodyRegistrationView(bodyId)); - assertNull(resource.getBodySnapshotIfRegistered(bodyId)); - resource.detachOwnerExecutor(owner); - } - } - - @Test - void queuedDestroyAfterCommandSpawnRemovesBodyBeforePublication() throws Exception { - RuntimeFixture fixture = createRuntimeFixture("owner-command-spawn-destroy-before-publication"); - PhysicsWorldRuntimeResource resource = fixture.resource(); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("owner-command-spawn-destroy-before-publication"); - resource.attachOwnerExecutor(owner); - - CountDownLatch blockerStarted = new CountDownLatch(1); - CountDownLatch releaseBlocker = new CountDownLatch(1); - owner.submitMutation("block command-buffer spawn before destroy", () -> { - blockerStarted.countDown(); - assertTrue(releaseBlocker.await(2, TimeUnit.SECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(blockerStarted.await(2, TimeUnit.SECONDS)); - - RigidBodyKey bodyId = RigidBodyKey.random(); - var spawnHandle = resource.submitCommands(361L, commands -> commands - .spawnBody(bodyId, spawn -> spawn - .space(fixture.spaceId()) - .box(0.5f, 0.5f, 0.5f) - .dynamic())); - CompletableFuture destroyFuture = - CompletableFuture.runAsync(() -> resource.destroyBody(bodyId)); - - assertTrue(resource.isBodyCreationPending(bodyId)); - assertFalse(spawnHandle.completion().toCompletableFuture().isDone()); - assertFalse(destroyFuture.isDone()); - - releaseBlocker.countDown(); - - var spawnResults = spawnHandle.completion() - .toCompletableFuture() - .get(2, TimeUnit.SECONDS); - destroyFuture.get(2, TimeUnit.SECONDS); - - assertEquals(PhysicsCommandResult.Status.APPLIED, spawnResults.getFirst().status()); - assertFalse(resource.isBodyCreationPending(bodyId)); - assertNull(resource.getBodyRegistrationView(bodyId)); - assertEquals(0, resource.query(new SpaceBodyCountQuery(fixture.spaceId())) - .completion() - .toCompletableFuture() - .get(2, TimeUnit.SECONDS)); - - PublishedPhysicsSnapshotFrame frame = resource.capturePublishedSnapshotFrame(1L, - 362L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - assertEquals(0, frame.bodyCount()); - resource.applyPublishedSnapshotFrame(frame); - assertFalse(resource.isBodyCreationPending(bodyId)); - assertNull(resource.getBodyRegistrationView(bodyId)); - resource.detachOwnerExecutor(owner); - } - } - - @Test - void ownerDetachPublishesCommandSpawnRegistrationViewsAndClearsPendingKeys() throws Exception { - RuntimeFixture fixture = createRuntimeFixture("owner-command-registration-detach"); - PhysicsWorldRuntimeResource resource = fixture.resource(); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(4, - Duration.ofSeconds(2L))) { - owner.start("owner-command-registration-detach"); - resource.attachOwnerExecutor(owner); - - RigidBodyKey bodyId = RigidBodyKey.random(); - var handle = resource.submitCommands(351L, commands -> commands - .spawnBody(bodyId, spawn -> spawn - .space(fixture.spaceId()) - .box(0.5f, 0.5f, 0.5f) - .dynamic())); - - handle.completion().toCompletableFuture().get(2, TimeUnit.SECONDS); - - assertNull(resource.getBodyRegistrationView(bodyId)); - assertTrue(resource.isBodyCreationPending(bodyId)); - - resource.detachOwnerExecutor(owner); - - assertNotNull(resource.getBodyRegistrationView(bodyId)); - assertFalse(resource.isBodyCreationPending(bodyId)); - } - } - - @Nonnull - private static RuntimeFixture createRuntimeFixture(@Nonnull String name) { - BackendId backendId = new BackendId("test:" + name + "-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerRuntimeProvider(new FakePhysicsBackendRuntimeProvider(backendId, - false, - false)); - PhysicsWorldRuntimeResource resource = new PhysicsWorldRuntimeResource(); - SpaceId spaceId = resource.createSpace(backendId, - "test-world", - PhysicsSpaceSettings.defaults()); - return new RuntimeFixture(resource, spaceId); - } - - private static void spawnPublishedBody(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyId) { - var handle = resource.submitCommands(0L, commands -> commands - .spawnBody(bodyId, spawn -> spawn - .space(spaceId) - .box(0.5f, 0.5f, 0.5f) - .dynamic() - .kind(PhysicsBodyKind.BODY) - .persistence(PhysicsBodyPersistenceMode.RUNTIME_ONLY))); - - var results = handle.completion().toCompletableFuture().join(); - assertEquals(PhysicsCommandResult.Status.APPLIED, results.getFirst().status()); - - PublishedPhysicsSnapshotFrame frame = resource.capturePublishedSnapshotFrame(1L, - 0L, - PublishedPhysicsSnapshotFrame.Status.COMPLETE, - 0L, - false); - resource.applyPublishedSnapshotFrame(frame); - assertNotNull(resource.getBodyRegistrationView(bodyId)); - assertFalse(resource.isBodyCreationPending(bodyId)); - } - - private record RuntimeFixture(@Nonnull PhysicsWorldRuntimeResource resource, - @Nonnull SpaceId spaceId) { - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/TestPhysicsOwnerLane.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/TestPhysicsOwnerLane.java deleted file mode 100644 index 2d391985..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/owner/TestPhysicsOwnerLane.java +++ /dev/null @@ -1,165 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.owner; - -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Test fixture that exposes a pooled owner lane as an ECS resource. - */ -public final class TestPhysicsOwnerLane implements PhysicsOwnerResource { - - private final int poolSize; - private final int queueCapacity; - @Nonnull - private final Duration closeTimeout; - private final PhysicsOwnerLaneScheduler scheduler; - private final PhysicsOwnerLaneResource lane; - - public TestPhysicsOwnerLane() { - this(2, - PhysicsOwnerLaneScheduler.DEFAULT_QUEUE_CAPACITY, - PhysicsOwnerLaneScheduler.DEFAULT_CLOSE_TIMEOUT); - } - - public TestPhysicsOwnerLane(int queueCapacity, @Nonnull Duration closeTimeout) { - this(2, queueCapacity, closeTimeout); - } - - public TestPhysicsOwnerLane(int poolSize, - int queueCapacity, - @Nonnull Duration closeTimeout) { - this.poolSize = poolSize; - this.queueCapacity = queueCapacity; - this.closeTimeout = closeTimeout; - scheduler = new PhysicsOwnerLaneScheduler(poolSize, queueCapacity, closeTimeout); - lane = scheduler.createLane(); - } - - @Override - public void start(@Nonnull String worldName) { - lane.start(worldName); - } - - @Override - public boolean isStarted() { - return lane.isStarted(); - } - - @Override - public boolean isClosed() { - return lane.isClosed(); - } - - @Override - public void close() { - scheduler.close(); - } - - @Nonnull - @Override - public PhysicsOwnerResult submitAndDrain(@Nonnull PhysicsOwnerCommand command) - throws InterruptedException, ExecutionException { - return lane.submitAndDrain(command); - } - - @Override - public boolean submitStepIfIdle(@Nonnull PhysicsOwnerCommand command) { - return lane.submitStepIfIdle(command); - } - - @Nonnull - @Override - public PhysicsMutationHandle submitMutation(@Nonnull String operation, - @Nonnull PhysicsOwnerCommand command) { - return lane.submitMutation(operation, command); - } - - @Nonnull - @Override - public PhysicsMutationHandle submitMutation(@Nonnull String operation, - @Nullable T value, - @Nonnull PhysicsOwnerCommand command) { - return lane.submitMutation(operation, value, command); - } - - @Nonnull - @Override - public CompletableFuture submitMutationFuture(@Nonnull String operation, - @Nonnull PhysicsOwnerCommand command) { - return lane.submitMutationFuture(operation, command); - } - - @Nonnull - @Override - public List pollCompletedMutations(int maxCompletions) { - return lane.pollCompletedMutations(maxCompletions); - } - - @Nullable - @Override - public PhysicsOwnerStepCompletion pollCompletedStep() { - return lane.pollCompletedStep(); - } - - @Override - public boolean hasPendingStep() { - return lane.hasPendingStep(); - } - - @Override - public long pendingStepAgeNanos() { - return lane.pendingStepAgeNanos(); - } - - @Override - public int pendingMutations() { - return lane.pendingMutations(); - } - - @Override - public int pendingCommands() { - return lane.pendingCommands(); - } - - @Override - public boolean isOwnerContext() { - return lane.isOwnerContext(); - } - - @Override - public void run(@Nonnull String operation, @Nonnull PhysicsOwnerMutation mutation) { - lane.run(operation, mutation); - } - - @Nonnull - @Override - public PhysicsMutationHandle enqueue(@Nonnull String operation, - @Nullable T value, - @Nonnull PhysicsOwnerMutation mutation) { - return lane.enqueue(operation, value, mutation); - } - - @Nonnull - @Override - public CompletableFuture enqueueCall(@Nonnull String operation, - @Nonnull PhysicsOwnerCallable callable) { - return lane.enqueueCall(operation, callable); - } - - @Nonnull - @Override - public T call(@Nonnull String operation, @Nonnull PhysicsOwnerCallable callable) { - return lane.call(operation, callable); - } - - @Nonnull - @Override - public PhysicsOwnerResource clone() { - return new TestPhysicsOwnerLane(poolSize, queueCapacity, closeTimeout); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/profiling/PhysicsRuntimeProfilingResourceTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/profiling/PhysicsRuntimeProfilingResourceTest.java index 5941bfe3..97d6971c 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/profiling/PhysicsRuntimeProfilingResourceTest.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/profiling/PhysicsRuntimeProfilingResourceTest.java @@ -42,8 +42,8 @@ void recordStepTracksLatestCumulativeAndWorstSamples() { assertEquals(9, resource.getCumulativeStep().getSubsteps()); assertEquals(140L, resource.getCumulativeStep().getTickNanos()); assertEquals(30L, resource.getCumulativeStep().getSnapshotNanos()); - assertEquals(10L, resource.getCumulativeStep().getOwnerQueuedNanos()); - assertEquals(90L, resource.getCumulativeStep().getOwnerRunNanos()); + assertEquals(10L, resource.getCumulativeStep().getStoreTickQueuedNanos()); + assertEquals(90L, resource.getCumulativeStep().getStoreTickRunNanos()); assertEquals(1, resource.getCumulativeStep().getSkippedPendingSteps()); assertEquals(75L, resource.getCumulativeStep().getPendingStepAgeNanos()); assertEquals(75L, resource.getCumulativeStep().getMaxPendingStepAgeNanos()); @@ -183,7 +183,7 @@ void finishSyncSampleCapturesCollectorMetricsAndClearsActiveCollector() { } @Test - void ownerStepRateUsesCompletedOwnerStepIntervals() { + void storeTickStepRateUsesCompletedStoreTickStepIntervals() { PhysicsRuntimeProfilingResource resource = new PhysicsRuntimeProfilingResource(); resource.recordStep(1, @@ -196,8 +196,8 @@ void ownerStepRateUsesCompletedOwnerStepIntervals() { 7L, 1_000_000_000L, PhysicsStepPhaseStats.unavailable()); - assertEquals(0, resource.getLatestStep().getOwnerStepRateSamples()); - assertEquals(0L, resource.getLatestStep().getOwnerStepIntervalNanos()); + assertEquals(0, resource.getLatestStep().getStoreTickStepRateSamples()); + assertEquals(0L, resource.getLatestStep().getStoreTickStepIntervalNanos()); resource.recordStep(1, 2, @@ -220,11 +220,12 @@ void ownerStepRateUsesCompletedOwnerStepIntervals() { 1_150_000_000L, PhysicsStepPhaseStats.unavailable()); - assertEquals(1, resource.getLatestStep().getOwnerStepRateSamples()); - assertEquals(100_000_000L, resource.getLatestStep().getOwnerStepIntervalNanos()); - assertEquals(2, resource.getCumulativeStep().getOwnerStepRateSamples()); - assertEquals(150_000_000L, resource.getCumulativeStep().getOwnerStepIntervalNanos()); - assertEquals(100_000_000L, resource.getCumulativeStep().getMaxOwnerStepIntervalNanos()); + assertEquals(1, resource.getLatestStep().getStoreTickStepRateSamples()); + assertEquals(100_000_000L, resource.getLatestStep().getStoreTickStepIntervalNanos()); + assertEquals(2, resource.getCumulativeStep().getStoreTickStepRateSamples()); + assertEquals(150_000_000L, resource.getCumulativeStep().getStoreTickStepIntervalNanos()); + assertEquals(100_000_000L, + resource.getCumulativeStep().getMaxStoreTickStepIntervalNanos()); resource.reset(); resource.recordStep(1, @@ -238,8 +239,8 @@ void ownerStepRateUsesCompletedOwnerStepIntervals() { 2_000_000_000L, PhysicsStepPhaseStats.unavailable()); - assertEquals(0, resource.getLatestStep().getOwnerStepRateSamples()); - assertEquals(0L, resource.getLatestStep().getOwnerStepIntervalNanos()); + assertEquals(0, resource.getLatestStep().getStoreTickStepRateSamples()); + assertEquals(0L, resource.getLatestStep().getStoreTickStepIntervalNanos()); } @Test diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/snapshot/PhysicsWorldSnapshotStateTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/snapshot/PhysicsWorldSnapshotStateTest.java deleted file mode 100644 index c4fcfb5a..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/snapshot/PhysicsWorldSnapshotStateTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.resources.snapshot; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldSnapshotState; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import org.junit.jupiter.api.Test; - -class PhysicsWorldSnapshotStateTest { - - @Test - void applyPublishedSnapshotFrameReportsWhetherFrameMatchesCurrentWorldEpoch() { - PhysicsWorldSnapshotState state = new PhysicsWorldSnapshotState(); - PublishedPhysicsSnapshotFrame currentFrame = PublishedPhysicsSnapshotFrame.empty(1L, 0L); - - PhysicsWorldSnapshotState.ApplyResult current = - state.applyPublishedSnapshotFrame(currentFrame); - - assertTrue(current.currentWorldEpoch()); - assertEquals(0, current.appliedCount()); - - state.markWorldChanged(); - - PhysicsWorldSnapshotState.ApplyResult stale = - state.applyPublishedSnapshotFrame(currentFrame); - - assertFalse(stale.currentWorldEpoch()); - assertEquals(0, stale.appliedCount()); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/visual/PhysicsVisualRuntimeTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/visual/PhysicsVisualRuntimeTest.java index 02968271..20dee980 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/visual/PhysicsVisualRuntimeTest.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/resources/visual/PhysicsVisualRuntimeTest.java @@ -7,7 +7,6 @@ import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; @@ -15,10 +14,10 @@ class PhysicsVisualRuntimeTest { - private static final RigidBodyKey FIRST_BODY = - RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000001")); - private static final RigidBodyKey SECOND_BODY = - RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000002")); + private static final UUID FIRST_BODY = + UUID.fromString("00000000-0000-0000-0000-000000000001"); + private static final UUID SECOND_BODY = + UUID.fromString("00000000-0000-0000-0000-000000000002"); @Test void hasAttachmentsPrunesStaleReferencesWithoutCopyingLiveAttachments() { @@ -27,29 +26,28 @@ void hasAttachmentsPrunesStaleReferencesWithoutCopyingLiveAttachments() { TestRef liveRef = new TestRef(true); TestRef staleRef = new TestRef(false); - assertFalse(runtime.hasAttachments(FIRST_BODY)); + assertFalse(runtime.hasAttachments(FIRST_BODY, null)); - runtime.registerAttachment(FIRST_BODY, staleRef); - runtime.registerAttachment(FIRST_BODY, liveRef); + runtime.registerAttachment(FIRST_BODY, null, staleRef); + runtime.registerAttachment(FIRST_BODY, null, liveRef); - assertTrue(runtime.hasAttachments(FIRST_BODY)); + assertTrue(runtime.hasAttachments(FIRST_BODY, null)); assertEquals(1, cleaned.get()); - runtime.unregisterAttachment(FIRST_BODY, liveRef); + runtime.unregisterAttachment(FIRST_BODY, null, liveRef); - assertFalse(runtime.hasAttachments(FIRST_BODY)); + assertFalse(runtime.hasAttachments(FIRST_BODY, null)); } @Test void generatedVisualProxyCountPrunesStaleReferencesWithoutBodyIdCopy() { AtomicInteger cleaned = new AtomicInteger(); PhysicsVisualRuntime runtime = new PhysicsVisualRuntime(_ -> cleaned.incrementAndGet()); - runtime.setGeneratedVisualProxy(FIRST_BODY, new TestRef(true)); - runtime.setGeneratedVisualProxy(SECOND_BODY, new TestRef(false)); + runtime.setGeneratedVisualProxy(FIRST_BODY, null, new TestRef(true)); + runtime.setGeneratedVisualProxy(SECOND_BODY, null, new TestRef(false)); assertEquals(1, runtime.generatedVisualProxyCount()); assertEquals(1, cleaned.get()); - assertEquals(1, runtime.getGeneratedVisualProxyBodyKeys().size()); } @Test @@ -62,10 +60,10 @@ void staleReferenceCleanerRunsOutsideVisualRuntimeLock() { }); runtimeRef.set(runtime); - runtime.registerAttachment(FIRST_BODY, new TestRef(false)); - runtime.setGeneratedVisualProxy(SECOND_BODY, new TestRef(false)); + runtime.registerAttachment(FIRST_BODY, null, new TestRef(false)); + runtime.setGeneratedVisualProxy(SECOND_BODY, null, new TestRef(false)); - assertFalse(runtime.hasAttachments(FIRST_BODY)); + assertFalse(runtime.hasAttachments(FIRST_BODY, null)); assertEquals(0, runtime.generatedVisualProxyCount()); assertEquals(2, cleaned.get()); } diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsCommandCompletionTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsCommandCompletionTest.java deleted file mode 100644 index ee6744c3..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsCommandCompletionTest.java +++ /dev/null @@ -1,67 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandBatch; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandCompletion; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandHandle; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandMetadata; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandResult; -import java.util.concurrent.CompletableFuture; -import org.junit.jupiter.api.Test; - -final class PhysicsCommandCompletionTest { - - @Test - void commandHandleSummariesUseCompletionSummary() { - PhysicsCommandMetadata metadata = new PhysicsCommandMetadata(10L, 7L); - PhysicsCommandCompletion completion = PhysicsCommandCompletion.allApplied(metadata, 10_000); - PhysicsCommandHandle handle = PhysicsCommandHandle.fromCompletionSummary( - new PhysicsCommandBatch(metadata, 10_000), - CompletableFuture.completedFuture(completion)); - - assertSame(completion, handle.completionSummary().toCompletableFuture().join()); - assertTrue(handle.allApplied().toCompletableFuture().join()); - assertTrue(handle.firstRejected().toCompletableFuture().join().isEmpty()); - } - - @Test - void compactAllAppliedCompletionExposesLazyResultList() { - PhysicsCommandMetadata metadata = new PhysicsCommandMetadata(10L, 7L); - - PhysicsCommandCompletion completion = PhysicsCommandCompletion.allApplied(metadata, 10_000); - - assertTrue(completion.allApplied()); - assertTrue(completion.firstRejected().isEmpty()); - assertEquals(10_000, completion.results().size()); - PhysicsCommandResult first = completion.results().getFirst(); - PhysicsCommandResult last = completion.results().get(9_999); - assertEquals(1L, first.commandSequence()); - assertEquals(10_000L, last.commandSequence()); - assertEquals(7L, first.commandBatchSequence()); - assertEquals(10L, last.submittedServerTick()); - } - - @Test - void compactAllRejectedCompletionExposesLazyResultList() { - PhysicsCommandMetadata metadata = new PhysicsCommandMetadata(11L, 8L); - - PhysicsCommandCompletion completion = PhysicsCommandCompletion.allRejected(metadata, - 10_000, - "stale batch"); - - assertFalse(completion.allApplied()); - PhysicsCommandResult firstRejected = completion.firstRejected().orElseThrow(); - assertEquals(1L, firstRejected.commandSequence()); - assertEquals("stale batch", firstRejected.message()); - assertEquals(10_000, completion.results().size()); - PhysicsCommandResult last = completion.results().get(9_999); - assertEquals(10_000L, last.commandSequence()); - assertEquals(PhysicsCommandResult.Status.REJECTED, last.status()); - assertEquals(8L, last.commandBatchSequence()); - assertEquals(11L, last.submittedServerTick()); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsCommandOperationsTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsCommandOperationsTest.java deleted file mode 100644 index f28ab613..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsCommandOperationsTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import org.junit.jupiter.api.Test; - -class PhysicsCommandOperationsTest { - - @Test - void requiredObjectAtReturnsTypedSlotValue() { - PhysicsCommandOperations operations = new PhysicsCommandOperations(1); - RigidBodyKey bodyKey = RigidBodyKey.random(); - operations.addDestroyBody(bodyKey); - - RigidBodyKey value = operations.requiredObjectAt(0, 0, RigidBodyKey.class); - - assertSame(bodyKey, value); - } - - @Test - void requiredObjectAtRejectsMissingSlotValue() { - PhysicsCommandOperations operations = new PhysicsCommandOperations(1); - operations.addDestroyJointBetween(null, - new SpaceId(1), - RigidBodyKey.random(), - RigidBodyKey.random()); - - IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, - () -> operations.requiredObjectAt(0, 0, JointKey.class)); - - assertEquals("Missing physics command object at index=0 slot=0 type=JointKey", - exception.getMessage()); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsRecipesTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsRecipesTest.java deleted file mode 100644 index 6cd0981d..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsRecipesTest.java +++ /dev/null @@ -1,165 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.simulation.batch.RecordedPhysicsCommandBatch; -import dev.hytalemodding.impulse.core.internal.simulation.recorder.MutablePhysicsCommandContext; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsFalloff; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsRecipes; -import dev.hytalemodding.impulse.core.plugin.snapshot.PhysicsBodySnapshotEntry; -import java.util.List; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class PhysicsRecipesTest { - - @Test - void applyImpulseRecipeCopiesVectorValuesAtConstructionTime() { - RigidBodyKey bodyKey = RigidBodyKey.of(0L, 701L); - Vector3f impulse = new Vector3f(1.0f, 2.0f, 3.0f); - - var recipe = PhysicsRecipes.applyImpulse(bodyKey, impulse); - impulse.set(9.0f, 9.0f, 9.0f); - - PhysicsCommandOperations operations = record(recipe).operations(); - - assertEquals(1, operations.size()); - assertEquals(PhysicsCommandOperations.APPLY_RIGID_BODY_IMPULSE, operations.opcode(0)); - assertEquals(bodyKey, - operations.requiredObjectAt(0, - PhysicsCommandOperations.BODY_COMMAND_BODY_KEY_OBJECT_SLOT, - RigidBodyKey.class)); - assertVector(operations, 0, 1.0f, 2.0f, 3.0f); - } - - @Test - void applyForceRecipeRecordsOneOperationPerBody() { - RigidBodyKey first = RigidBodyKey.of(0L, 702L); - RigidBodyKey second = RigidBodyKey.of(0L, 703L); - - PhysicsCommandOperations operations = record( - PhysicsRecipes.applyForce(List.of(first, second), new Vector3f(4.0f, 5.0f, 6.0f))) - .operations(); - - assertEquals(2, operations.size()); - assertEquals(PhysicsCommandOperations.APPLY_RIGID_BODY_FORCE, operations.opcode(0)); - assertEquals(PhysicsCommandOperations.APPLY_RIGID_BODY_FORCE, operations.opcode(1)); - assertEquals(first, - operations.requiredObjectAt(0, - PhysicsCommandOperations.BODY_COMMAND_BODY_KEY_OBJECT_SLOT, - RigidBodyKey.class)); - assertEquals(second, - operations.requiredObjectAt(1, - PhysicsCommandOperations.BODY_COMMAND_BODY_KEY_OBJECT_SLOT, - RigidBodyKey.class)); - assertVector(operations, 0, 4.0f, 5.0f, 6.0f); - assertVector(operations, 1, 4.0f, 5.0f, 6.0f); - } - - @Test - void radialImpulseUsesSnapshotsFalloffAndDynamicBodiesOnly() { - RigidBodyKey near = RigidBodyKey.of(0L, 704L); - RigidBodyKey far = RigidBodyKey.of(0L, 705L); - RigidBodyKey outside = RigidBodyKey.of(0L, 706L); - RigidBodyKey staticBody = RigidBodyKey.of(0L, 707L); - List entries = List.of( - entry(near, 1.0f, PhysicsBodyType.DYNAMIC), - entry(far, 3.0f, PhysicsBodyType.DYNAMIC), - entry(outside, 6.0f, PhysicsBodyType.DYNAMIC), - entry(staticBody, 1.0f, PhysicsBodyType.STATIC)); - - PhysicsCommandOperations operations = record(PhysicsRecipes.radialImpulse(entries, - new Vector3f(), - 10.0f, - 5.0f, - PhysicsFalloff.linear())).operations(); - - assertEquals(2, operations.size()); - assertEquals(near, - operations.requiredObjectAt(0, - PhysicsCommandOperations.BODY_COMMAND_BODY_KEY_OBJECT_SLOT, - RigidBodyKey.class)); - assertEquals(far, - operations.requiredObjectAt(1, - PhysicsCommandOperations.BODY_COMMAND_BODY_KEY_OBJECT_SLOT, - RigidBodyKey.class)); - assertVector(operations, 0, 8.0f, 0.0f, 0.0f); - assertVector(operations, 1, 4.0f, 0.0f, 0.0f); - } - - private static RecordedPhysicsCommandBatch record( - dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandRecipe recipe) { - MutablePhysicsCommandContext context = new MutablePhysicsCommandContext(1L, 1L); - context.compose(recipe); - return context.freezeInternal(1L); - } - - private static PhysicsBodySnapshotEntry entry(RigidBodyKey bodyKey, - float positionX, - PhysicsBodyType bodyType) { - return new PhysicsBodySnapshotEntry(bodyKey, - PhysicsBodySnapshot.of(positionX, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 1.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - bodyType, - false, - false, - 1.0f, - 0.5f, - 0.0f, - 0.0f, - 0.0f, - 1, - 1, - false, - 0.0f, - ShapeType.BOX, - true, - 0.5f, - 0.5f, - 0.5f, - 0.0f, - 0.0f, - PhysicsAxis.Y), - new SpaceId(1), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - } - - private static void assertVector(PhysicsCommandOperations operations, - int operationIndex, - float x, - float y, - float z) { - assertEquals(x, - operations.floatAt(operationIndex, - PhysicsCommandOperations.VECTOR_COMMAND_X_FLOAT_SLOT), - 0.0001f); - assertEquals(y, - operations.floatAt(operationIndex, - PhysicsCommandOperations.VECTOR_COMMAND_Y_FLOAT_SLOT), - 0.0001f); - assertEquals(z, - operations.floatAt(operationIndex, - PhysicsCommandOperations.VECTOR_COMMAND_Z_FLOAT_SLOT), - 0.0001f); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsSimulationExecutorTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsSimulationExecutorTest.java deleted file mode 100644 index fb385aca..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/simulation/PhysicsSimulationExecutorTest.java +++ /dev/null @@ -1,812 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.simulation; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsContact; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend.InMemoryPhysicsSpace; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.WorldVoxelCollisionCache; -import dev.hytalemodding.impulse.core.internal.simulation.query.BenchmarkSpaceStatsQuery; -import dev.hytalemodding.impulse.core.internal.simulation.query.PhysicsDebugContactsQuery; -import dev.hytalemodding.impulse.core.internal.simulation.query.PhysicsDebugJointsQuery; -import dev.hytalemodding.impulse.core.internal.simulation.view.BenchmarkSpaceStatsView; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsDebugContactView; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsDebugJointView; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandResult; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RaycastClosestBatchResult; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RaycastClosestBatchQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.RaycastSegment; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Quaternionf; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class PhysicsSimulationExecutorTest { - - private static final AtomicInteger BACKEND_COUNTER = new AtomicInteger(); - - @Test - void templatedBulkSpawnExecutesAsSingleCommandResult() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:bulk-spawn-executor-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - SpaceId spaceId = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()) - .id(); - RigidBodyKey first = RigidBodyKey.random(); - RigidBodyKey second = RigidBodyKey.random(); - - List results = resource.submitCommands(1L, 1, commands -> commands.spawnBodies(2, - spaceId, - PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f), - 1.0f, - PhysicsBodyType.DYNAMIC, - RigidBodySpawnSettings.defaults(), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY, - spawns -> spawns - .body(first, 1.0f, 2.0f, 3.0f) - .body(second, 4.0f, 5.0f, 6.0f))) - .completion() - .toCompletableFuture() - .join(); - - assertEquals(1, results.size()); - assertEquals(PhysicsCommandResult.Status.APPLIED, results.getFirst().status()); - assertEquals(2, resource.getBodyRegistrationCount()); - } - - @Test - void forceAndTorqueCommandsUseScalarBodyApi() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:scalar-body-force-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - SpaceId spaceId = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()) - .id(); - RigidBodyKey bodyKey = RigidBodyKey.random(); - ScalarRecordingBody body = new ScalarRecordingBody(); - resource.addBodyOnOwner(bodyKey, - spaceId, - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - - resource.submitCommands(1L, 4, commands -> commands - .applyBodyImpulse(bodyKey, 1.0f, 2.0f, 3.0f, 0.1f, 0.2f, 0.3f) - .applyBodyTorqueImpulse(bodyKey, 4.0f, 5.0f, 6.0f) - .applyBodyForce(bodyKey, 7.0f, 8.0f, 9.0f, 0.4f, 0.5f, 0.6f) - .applyBodyTorque(bodyKey, 10.0f, 11.0f, 12.0f)) - .completion() - .toCompletableFuture() - .join(); - - assertEquals(1, body.scalarImpulseCalls); - assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), body.lastImpulse); - assertEquals(new Vector3f(0.1f, 0.2f, 0.3f), body.lastImpulseOffset); - assertEquals(1, body.scalarTorqueImpulseCalls); - assertEquals(new Vector3f(4.0f, 5.0f, 6.0f), body.lastTorqueImpulse); - assertEquals(1, body.scalarForceCalls); - assertEquals(new Vector3f(7.0f, 8.0f, 9.0f), body.lastForce); - assertEquals(new Vector3f(0.4f, 0.5f, 0.6f), body.lastForceOffset); - assertEquals(1, body.scalarTorqueCalls); - assertEquals(new Vector3f(10.0f, 11.0f, 12.0f), body.lastTorque); - assertEquals(4, body.activateCalls); - } - - @Test - void transformCommandsUseScalarRotationApi() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:scalar-transform-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - SpaceId spaceId = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()) - .id(); - RigidBodyKey bodyKey = RigidBodyKey.random(); - ScalarRecordingBody body = new ScalarRecordingBody(); - resource.addBodyOnOwner(bodyKey, - spaceId, - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - Quaternionf rotation = new Quaternionf().rotateXYZ(0.1f, 0.2f, 0.3f); - resource.submitCommands(1L, 1, commands -> commands.setBodyTransform(bodyKey, - 1.0f, - 2.0f, - 3.0f, - rotation.x, - rotation.y, - rotation.z, - rotation.w, - true)).completion().toCompletableFuture().join(); - - assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), body.position); - assertEquals(1, body.scalarRotationCalls); - assertEquals(0, body.objectRotationCalls); - assertEquals(1, body.activateCalls); - } - - @Test - void positionCommandsUseScalarBodyApiWithoutTouchingRotation() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:scalar-position-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - SpaceId spaceId = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()) - .id(); - RigidBodyKey bodyKey = RigidBodyKey.random(); - ScalarRecordingBody body = new ScalarRecordingBody(); - resource.addBodyOnOwner(bodyKey, - spaceId, - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - - resource.submitCommands(1L, 1, commands -> commands.setBodyPosition(bodyKey, - 5.0f, - 6.0f, - 7.0f, - true)) - .completion() - .toCompletableFuture() - .join(); - - assertEquals(new Vector3f(5.0f, 6.0f, 7.0f), body.position); - assertEquals(1, body.scalarPositionCalls); - assertEquals(0, body.objectPositionCalls); - assertEquals(0, body.scalarRotationCalls); - assertEquals(0, body.objectRotationCalls); - assertEquals(1, body.activateCalls); - } - - @Test - void debugContactQueryReturnsPrimitiveCopiedViewsWithinRadius() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:debug-contact-query-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - InMemoryPhysicsSpace space = (InMemoryPhysicsSpace) resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - PhysicsBody bodyA = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody bodyB = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - resource.addBody(space.id(), - bodyA, - PhysicsBodyKind.TEMPORARY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - resource.addBody(space.id(), - bodyB, - PhysicsBodyKind.TEMPORARY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - space.addContact(new PhysicsContact(bodyA, - bodyB, - new Vector3f(0.0f, 0.0f, 0.0f), - new Vector3f(1.0f, 2.0f, 3.0f), - new Vector3f(0.0f, 2.0f, 0.0f), - -0.1f, - 20.0f)); - - List visible = resource.queryInternal(new PhysicsDebugContactsQuery(space.id(), - 1.0f, - 2.0f, - 3.0f, - 0.5f, - 4)) - .toCompletableFuture() - .join(); - - assertEquals(1, visible.size()); - PhysicsDebugContactView contact = visible.getFirst(); - assertEquals(1.0f, contact.pointX(), 0.00001f); - assertEquals(2.0f, contact.pointY(), 0.00001f); - assertEquals(3.0f, contact.pointZ(), 0.00001f); - assertTrue(contact.hasNormal()); - assertEquals(0.0f, contact.normalX(), 0.00001f); - assertEquals(1.0f, contact.normalY(), 0.00001f); - assertEquals(0.0f, contact.normalZ(), 0.00001f); - - assertTrue(resource.queryInternal(new PhysicsDebugContactsQuery(space.id(), - 3.0f, - 2.0f, - 3.0f, - 0.5f, - 4)) - .toCompletableFuture() - .join() - .isEmpty()); - assertTrue(resource.queryInternal(new PhysicsDebugContactsQuery(space.id(), - 1.0f, - 2.0f, - 3.0f, - 0.5f, - 0)) - .toCompletableFuture() - .join() - .isEmpty()); - } - - @Test - void debugJointQueryReturnsPrimitiveCopiedViewsWithinRadius() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:debug-joint-query-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - InMemoryPhysicsSpace space = (InMemoryPhysicsSpace) resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - PhysicsBody bodyA = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody bodyB = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - bodyA.setPosition(1.0f, 0.0f, 0.0f); - bodyB.setPosition(5.0f, 0.0f, 0.0f); - resource.addBody(space.id(), - bodyA, - PhysicsBodyKind.TEMPORARY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - resource.addBody(space.id(), - bodyB, - PhysicsBodyKind.TEMPORARY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - var liveJoint = space.createHingeJoint(bodyA, - bodyB, - new Vector3f(1.0f, 0.0f, 0.0f), - new Vector3f(-1.0f, 0.0f, 0.0f), - new Vector3f(0.0f, 1.0f, 0.0f)); - resource.addJoint(space.id(), liveJoint); - - List visible = resource.queryInternal(new PhysicsDebugJointsQuery(space.id(), - 3.0f, - 0.0f, - 0.0f, - 1.0f, - 4)) - .toCompletableFuture() - .join(); - - assertEquals(1, visible.size()); - PhysicsDebugJointView joint = visible.getFirst(); - assertEquals(2.0f, joint.anchorAX(), 0.00001f); - assertEquals(0.0f, joint.anchorAY(), 0.00001f); - assertEquals(0.0f, joint.anchorAZ(), 0.00001f); - assertEquals(4.0f, joint.anchorBX(), 0.00001f); - assertEquals(0.0f, joint.anchorBY(), 0.00001f); - assertEquals(0.0f, joint.anchorBZ(), 0.00001f); - assertTrue(joint.hasAxis()); - assertEquals(0.0f, joint.axisX(), 0.00001f); - assertEquals(0.9f, joint.axisY(), 0.00001f); - assertEquals(0.0f, joint.axisZ(), 0.00001f); - - assertTrue(resource.queryInternal(new PhysicsDebugJointsQuery(space.id(), - 8.0f, - 0.0f, - 0.0f, - 1.0f, - 4)) - .toCompletableFuture() - .join() - .isEmpty()); - assertTrue(resource.queryInternal(new PhysicsDebugJointsQuery(space.id(), - 3.0f, - 0.0f, - 0.0f, - 1.0f, - 0)) - .toCompletableFuture() - .join() - .isEmpty()); - } - - @Test - void raycastClosestBatchReturnsMissesByIndex() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:raycast-batch-result-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - SpaceId spaceId = resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()) - .id(); - - RaycastClosestBatchResult result = resource.query(new RaycastClosestBatchQuery(spaceId, - List.of(new RaycastSegment(0.0f, 1.0f, 0.0f, 0.0f, -1.0f, 0.0f), - new RaycastSegment(1.0f, 1.0f, 0.0f, 1.0f, -1.0f, 0.0f)))) - .completion() - .toCompletableFuture() - .join(); - - assertEquals(2, result.rayCount()); - assertEquals(0, result.hitCount()); - assertFalse(result.hasHit(0)); - assertNull(result.hit(0)); - } - - @Test - void benchmarkSpaceStatsQueryClassifiesBodiesWithOneOwnerAggregate() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:benchmark-space-stats-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - InMemoryPhysicsSpace space = (InMemoryPhysicsSpace) resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - PhysicsBody registered = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - registered.setPosition(0.0f, 124.0f, 0.0f); - resource.addBodyOnOwner(RigidBodyKey.random(), - space.id(), - registered, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - PhysicsBody raw = space.createSphere(1.0f, 1.0f); - raw.setPosition(0.0f, -40.0f, 0.0f); - resource.addBody(space.id(), - raw, - PhysicsBodyKind.TEMPORARY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - resource.addBody(space.id(), - space.createStaticPlane(122.0f), - PhysicsBodyKind.WORLD_COLLISION, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - - BenchmarkSpaceStatsView stats = resource.queryInternal(new BenchmarkSpaceStatsQuery(space.id(), - 122.0f, - 1.0f, - -32.0f, - -128.0f, - false)) - .toCompletableFuture() - .join(); - - assertEquals(3, stats.bodies()); - assertEquals(2, stats.dynamicBodies()); - assertEquals(2, stats.awakeDynamicBodies()); - assertEquals(0, stats.sleepingDynamicBodies()); - assertEquals(1, stats.detachedBodies()); - assertEquals(1, stats.rawBodies()); - assertEquals(0, stats.worldCollisionBodies()); - assertEquals(1, stats.belowPlaneBodies()); - assertEquals(1, stats.belowWorldMinBodies()); - assertEquals(0, stats.belowVoidBodies()); - assertEquals(-40.0, stats.minDynamicBodyY(), 0.00001); - assertEquals(124.0, stats.maxDynamicBodyY(), 0.00001); - } - - @Test - void benchmarkSpaceStatsQueryCountsCacheOwnedWorldCollisionBodies() throws Exception { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:benchmark-space-stats-cache-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - InMemoryPhysicsSpace space = (InMemoryPhysicsSpace) resource.createLiveSpace(backend.getId(), - "test-world", - PhysicsSpaceSettings.defaults()); - putCachedWorldCollisionBody(resource.worldCollisionCache(), space.id(), 42L); - - BenchmarkSpaceStatsView stats = resource.queryInternal(new BenchmarkSpaceStatsQuery(space.id(), - 122.0f, - 1.0f, - -32.0f, - -128.0f, - false)) - .toCompletableFuture() - .join(); - - assertEquals(1, stats.bodies()); - assertEquals(1, stats.worldCollisionBodies()); - assertEquals(0, stats.rawBodies()); - } - - @SuppressWarnings("unchecked") - private static void putCachedWorldCollisionBody(@Nonnull WorldVoxelCollisionCache worldCache, - @Nonnull SpaceId spaceId, - long backendBodyId) throws Exception { - Object spaceCache = newSpaceCollisionCache(); - Object section = newCachedSection(); - Field backendBodyIds = section.getClass().getDeclaredField("backendBodyIds"); - backendBodyIds.setAccessible(true); - ((List) backendBodyIds.get(section)).add(backendBodyId); - putCachedSection(spaceCache, section); - - Field spaces = WorldVoxelCollisionCache.class.getDeclaredField("spaces"); - spaces.setAccessible(true); - ((Map) spaces.get(worldCache)).put(spaceId.value(), spaceCache); - } - - private static Object newSpaceCollisionCache() throws Exception { - Constructor constructor = nestedCacheClass("SpaceCollisionCache").getDeclaredConstructor(); - constructor.setAccessible(true); - return constructor.newInstance(); - } - - private static Object newCachedSection() throws Exception { - Constructor constructor = nestedCacheClass("CachedSection") - .getDeclaredConstructor(int.class, int.class, int.class, long.class, long.class); - constructor.setAccessible(true); - return constructor.newInstance(0, 0, 0, 0L, 1L); - } - - @SuppressWarnings("unchecked") - private static void putCachedSection(@Nonnull Object cache, @Nonnull Object section) throws Exception { - Method keyMethod = WorldVoxelCollisionCache.class.getDeclaredMethod("packSectionKey", - int.class, - int.class, - int.class); - keyMethod.setAccessible(true); - long key = (long) keyMethod.invoke(null, - intField(section, "chunkX"), - intField(section, "sectionY"), - intField(section, "chunkZ")); - Field sections = cache.getClass().getDeclaredField("sections"); - sections.setAccessible(true); - ((Map) sections.get(cache)).put(key, section); - } - - private static int intField(@Nonnull Object target, @Nonnull String fieldName) throws Exception { - Field field = target.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - return field.getInt(target); - } - - @Nonnull - private static Class nestedCacheClass(@Nonnull String simpleName) { - return Arrays.stream(WorldVoxelCollisionCache.class.getDeclaredClasses()) - .filter(candidate -> candidate.getSimpleName().equals(simpleName)) - .findFirst() - .orElseThrow(); - } - - private static final class ScalarRecordingBody implements PhysicsBody { - - private final Vector3f position = new Vector3f(); - private final Quaternionf rotation = new Quaternionf(); - private final Vector3f linearVelocity = new Vector3f(); - private final Vector3f angularVelocity = new Vector3f(); - private PhysicsBodyType bodyType = PhysicsBodyType.DYNAMIC; - private int activateCalls; - private int scalarForceCalls; - private int scalarImpulseCalls; - private int scalarPositionCalls; - private int scalarRotationCalls; - private int scalarTorqueCalls; - private int scalarTorqueImpulseCalls; - private int objectPositionCalls; - private int objectRotationCalls; - private final Vector3f lastForce = new Vector3f(); - private final Vector3f lastForceOffset = new Vector3f(); - private final Vector3f lastImpulse = new Vector3f(); - private final Vector3f lastImpulseOffset = new Vector3f(); - private final Vector3f lastTorque = new Vector3f(); - private final Vector3f lastTorqueImpulse = new Vector3f(); - - @Override - public void setPosition(float x, float y, float z) { - scalarPositionCalls++; - position.set(x, y, z); - } - - @Override - public void setPosition(@Nonnull Vector3f pos) { - objectPositionCalls++; - position.set(pos); - } - - @Nonnull - @Override - public Vector3f getPosition() { - return new Vector3f(position); - } - - @Override - public void setRotation(float x, float y, float z, float w) { - scalarRotationCalls++; - rotation.set(x, y, z, w); - } - - @Override - public void setRotation(@Nonnull Quaternionf rot) { - objectRotationCalls++; - rotation.set(rot); - } - - @Nonnull - @Override - public Quaternionf getRotation() { - return new Quaternionf(rotation); - } - - @Override - public void setRestitution(float restitution) { - } - - @Override - public float getRestitution() { - return 0.0f; - } - - @Override - public void setFriction(float friction) { - } - - @Override - public float getFriction() { - return 0.0f; - } - - @Nonnull - @Override - public PhysicsBodyType getBodyType() { - return bodyType; - } - - @Override - public void setBodyType(@Nonnull PhysicsBodyType bodyType) { - this.bodyType = bodyType; - } - - @Override - public boolean isStatic() { - return bodyType == PhysicsBodyType.STATIC; - } - - @Override - public boolean isKinematic() { - return bodyType == PhysicsBodyType.KINEMATIC; - } - - @Override - public void setKinematic(boolean kinematic) { - bodyType = kinematic ? PhysicsBodyType.KINEMATIC : PhysicsBodyType.DYNAMIC; - } - - @Override - public void activate() { - activateCalls++; - } - - @Override - public boolean isActive() { - return true; - } - - @Override - public boolean isSleeping() { - return false; - } - - @Override - public void sleep() { - } - - @Override - public float getMass() { - return 1.0f; - } - - @Override - public void setMass(float mass) { - } - - @Nonnull - @Override - public Vector3f getLinearVelocity() { - return new Vector3f(linearVelocity); - } - - @Override - public void setLinearVelocity(@Nonnull Vector3f vel) { - linearVelocity.set(vel); - } - - @Override - public void setLinearVelocity(float x, float y, float z) { - linearVelocity.set(x, y, z); - } - - @Nonnull - @Override - public Vector3f getAngularVelocity() { - return new Vector3f(angularVelocity); - } - - @Override - public void setAngularVelocity(@Nonnull Vector3f vel) { - angularVelocity.set(vel); - } - - @Override - public void setAngularVelocity(float x, float y, float z) { - angularVelocity.set(x, y, z); - } - - @Override - public float getLinearDamping() { - return 0.0f; - } - - @Override - public void setLinearDamping(float damping) { - } - - @Override - public float getAngularDamping() { - return 0.0f; - } - - @Override - public void setAngularDamping(float damping) { - } - - @Override - public void applyCentralForce(@Nonnull Vector3f force) { - throw new AssertionError("central vector force should not be used"); - } - - @Override - public void applyCentralForce(float x, float y, float z) { - } - - @Override - public void applyForce(@Nonnull Vector3f force, @Nonnull Vector3f offset) { - throw new AssertionError("offset vector force should not be used"); - } - - @Override - public void applyForce(float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ) { - scalarForceCalls++; - lastForce.set(x, y, z); - lastForceOffset.set(offsetX, offsetY, offsetZ); - } - - @Override - public void applyCentralImpulse(@Nonnull Vector3f impulse) { - throw new AssertionError("central vector impulse should not be used"); - } - - @Override - public void applyCentralImpulse(float x, float y, float z) { - } - - @Override - public void applyImpulse(@Nonnull Vector3f impulse, @Nonnull Vector3f offset) { - throw new AssertionError("offset vector impulse should not be used"); - } - - @Override - public void applyImpulse(float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ) { - scalarImpulseCalls++; - lastImpulse.set(x, y, z); - lastImpulseOffset.set(offsetX, offsetY, offsetZ); - } - - @Override - public void applyTorque(@Nonnull Vector3f torque) { - throw new AssertionError("vector torque should not be used"); - } - - @Override - public void applyTorque(float x, float y, float z) { - scalarTorqueCalls++; - lastTorque.set(x, y, z); - } - - @Override - public void applyTorqueImpulse(@Nonnull Vector3f torqueImpulse) { - throw new AssertionError("vector torque impulse should not be used"); - } - - @Override - public void applyTorqueImpulse(float x, float y, float z) { - scalarTorqueImpulseCalls++; - lastTorqueImpulse.set(x, y, z); - } - - @Override - public void clearForces() { - } - - @Override - public boolean isSensor() { - return false; - } - - @Override - public void setSensor(boolean sensor) { - } - - @Override - public int getCollisionGroup() { - return 0; - } - - @Override - public int getCollisionMask() { - return 0; - } - - @Override - public void setCollisionFilter(int group, int mask) { - } - - @Override - public boolean isContinuousCollisionEnabled() { - return false; - } - - @Override - public void setContinuousCollisionEnabled(boolean enabled) { - } - - @Nonnull - @Override - public ShapeType getShapeType() { - return ShapeType.SPHERE; - } - - @Nullable - @Override - public Vector3f getBoxHalfExtents() { - return null; - } - - @Override - public float getSphereRadius() { - return 0.5f; - } - - @Override - public float getHalfHeight() { - return 0.0f; - } - - @Nonnull - @Override - public PhysicsAxis getShapeAxis() { - return PhysicsAxis.Y; - } - - @Override - public float getCenterOfMassOffsetY() { - return 0.0f; - } - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/BodyBindingSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/BodyBindingSystemTest.java new file mode 100644 index 00000000..ba24577c --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/BodyBindingSystemTest.java @@ -0,0 +1,425 @@ +package dev.hytalemodding.impulse.core.internal.systems; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.*; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.FakePhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkLifecycle; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkStoreTypes; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionRestoreDependencyComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.components.ChunkCollisionSourceComponent.PartKind; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.systems.ChunkCollisionMutationDrainSystem; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkSettingsIndexResource; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.resources.PhysicsChunkSettingsIndexResource.PhysicsChunkSpaceSettings; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.systems.binding.BodyBindingSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.SpaceBindingSystem; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.EntityChunkBoundaryMode; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import java.util.ArrayList; +import java.util.Map; +import java.util.UUID; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class BodyBindingSystemTest { + + private static final BackendId BACKEND_ID = new BackendId("test:body-binding-no-physicschunk"); + + @Test + void nonVoxelBodyBindingDoesNotRequirePhysicsChunkPayloadResource() { + PhysicsChunkStoreTypes.clearPhysicsStoreResourceTypes(); + FakePhysicsBackendRuntimeProvider provider = + new FakePhysicsBackendRuntimeProvider(BACKEND_ID, false, false); + ImpulseBackendRegistry.registerRuntimeProvider(provider); + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + proxy.registerSystem(new PersistenceHydrationSystem()); + proxy.registerSystem(new IdentityIndexSystem()); + proxy.registerSystem(new SpaceBindingSystem()); + proxy.registerSystem(new SpaceSettingsApplicationSystem()); + proxy.registerSystem(new BodyBindingSystem()); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("body-binding-no-physicschunk-test")), + EmptyResourceStorage.get()); + try { + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + restore.markComplete(); + restore.markHydrated(); + Ref spaceRef = addSpace(store, uuid(1)); + Ref bodyRef = addBody(store, uuid(2), uuid(1), spaceRef); + + assertDoesNotThrow(() -> store.tick(0.0f)); + + assertFalse(restore.isFailed(), restore.getFailureMessage()); + assertEquals(0, restore.getSoftSkipsByReason().size()); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + BackendSpaceHandle spaceHandle = runtime.getSpaceHandle(spaceRef); + BackendBodyHandle bodyHandle = runtime.getBodyHandle(bodyRef); + assertNotNull(spaceHandle); + assertNotNull(bodyHandle); + FakePhysicsBackendRuntime backendRuntime = provider.createdRuntimes().get(0); + assertEquals(1, backendRuntime.bodyCount(spaceHandle.value())); + } finally { + if (!store.isShutdown()) { + registry.removeStore(store); + } + registry.shutdown(); + PhysicsChunkStoreTypes.clearPhysicsStoreResourceTypes(); + } + } + + @Test + void nonVoxelBodyBindingSeedsInitialPropertiesWithoutSeparateMutationCalls() { + PhysicsChunkStoreTypes.clearPhysicsStoreResourceTypes(); + FakePhysicsBackendRuntimeProvider provider = + new FakePhysicsBackendRuntimeProvider(BACKEND_ID, true, false); + ImpulseBackendRegistry.registerRuntimeProvider(provider); + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + proxy.registerSystem(new PersistenceHydrationSystem()); + proxy.registerSystem(new IdentityIndexSystem()); + proxy.registerSystem(new SpaceBindingSystem()); + proxy.registerSystem(new SpaceSettingsApplicationSystem()); + proxy.registerSystem(new BodyBindingSystem()); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("body-binding-configured-create-test")), + EmptyResourceStorage.get()); + try { + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + restore.markComplete(); + restore.markHydrated(); + UUID spaceUuid = uuid(1); + Ref spaceRef = addSpace(store, spaceUuid); + Ref bodyRef = addConfiguredBody(store, uuid(2), spaceUuid, spaceRef); + + store.tick(0.0f); + + assertFalse(restore.isFailed(), restore.getFailureMessage()); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + BackendSpaceHandle spaceHandle = runtime.getSpaceHandle(spaceRef); + BackendBodyHandle bodyHandle = runtime.getBodyHandle(bodyRef); + assertNotNull(spaceHandle); + assertNotNull(bodyHandle); + FakePhysicsBackendRuntime backendRuntime = provider.createdRuntimes().get(0); + assertEquals(1, backendRuntime.createBodyWithInitialStateCalls()); + assertEquals(0, backendRuntime.setBodyDampingCalls()); + assertEquals(0, backendRuntime.setBodyFrictionCalls()); + assertEquals(0, backendRuntime.setBodyRestitutionCalls()); + assertEquals(0, backendRuntime.setBodyCollisionFilterCalls()); + assertEquals(0, backendRuntime.setBodySensorCalls()); + assertEquals(0, backendRuntime.setBodyContinuousCollisionCalls()); + assertEquals(0.2f, + backendRuntime.bodyLinearDamping(spaceHandle.value(), bodyHandle.value())); + assertEquals(0.3f, + backendRuntime.bodyAngularDamping(spaceHandle.value(), bodyHandle.value())); + assertEquals(0.72f, backendRuntime.bodyFriction(spaceHandle.value(), bodyHandle.value())); + assertEquals(0.18f, + backendRuntime.bodyRestitution(spaceHandle.value(), bodyHandle.value())); + assertEquals(PhysicsCollisionFilters.TERRAIN, + backendRuntime.bodyCollisionGroup(spaceHandle.value(), bodyHandle.value())); + assertEquals(PhysicsCollisionFilters.DYNAMIC_BODY, + backendRuntime.bodyCollisionMask(spaceHandle.value(), bodyHandle.value())); + assertTrue(backendRuntime.bodySensor(spaceHandle.value(), bodyHandle.value())); + assertTrue(backendRuntime.isBodyContinuousCollisionEnabled(spaceHandle.value(), + bodyHandle.value())); + } finally { + if (!store.isShutdown()) { + registry.removeStore(store); + } + registry.shutdown(); + PhysicsChunkStoreTypes.clearPhysicsStoreResourceTypes(); + } + } + + @Test + void chunkRestoreDependencyWaitsWithoutBlockingUnrelatedBodies() { + PhysicsChunkLifecycle.enable(); + FakePhysicsBackendRuntimeProvider provider = + new FakePhysicsBackendRuntimeProvider(BACKEND_ID, false, false); + ImpulseBackendRegistry.registerRuntimeProvider(provider); + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + PhysicsChunkStoreTypes.registerPhysicsStoreResourceTypes(proxy); + proxy.registerSystem(new PersistenceHydrationSystem()); + proxy.registerSystem(new IdentityIndexSystem()); + proxy.registerSystem(new SpaceBindingSystem()); + proxy.registerSystem(new SpaceSettingsApplicationSystem()); + proxy.registerSystem(new BodyBindingSystem()); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("body-binding-restore-context-test")), + EmptyResourceStorage.get()); + try { + UUID spaceUuid = uuid(1); + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + restore.markComplete(); + restore.markHydrated(); + Ref spaceRef = addSpace(store, spaceUuid); + store.getResource(PhysicsChunkSettingsIndexResource.getResourceType()) + .replaceAll(Map.of(spaceUuid, + new PhysicsChunkSpaceSettings(spaceUuid, + PhysicsChunkCollisionMode.STREAMING, + EntityChunkBoundaryMode.PAUSE_UNTIL_LOADED, + false, + 8, + 4, + 100))); + Ref waitingBodyRef = addBody(store, uuid(2), spaceUuid, spaceRef); + store.putComponent(waitingBodyRef, + ChunkCollisionRestoreDependencyComponent.getComponentType(), + new ChunkCollisionRestoreDependencyComponent(spaceUuid, + new Vector3f(1.0f, 2.0f, 3.0f), + 4, + PhysicsChunkCollisionMode.STREAMING)); + Ref ordinaryBodyRef = addBody(store, uuid(3), spaceUuid, spaceRef); + + store.tick(0.0f); + + assertFalse(restore.isFailed(), restore.getFailureMessage()); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + assertNotNull(runtime.getBodyHandle(ordinaryBodyRef)); + assertNull(runtime.getBodyHandle(waitingBodyRef)); + assertEquals(1, + restore.getSoftSkipsByReason() + .getInt("Body restore dependency pending chunk collision")); + BackendSpaceHandle spaceHandle = runtime.getSpaceHandle(spaceRef); + assertNotNull(spaceHandle); + assertEquals(1, provider.createdRuntimes().get(0).bodyCount(spaceHandle.value())); + } finally { + if (!store.isShutdown()) { + registry.removeStore(store); + } + registry.shutdown(); + PhysicsChunkStoreTypes.clearPhysicsStoreResourceTypes(); + PhysicsChunkLifecycle.disable(); + } + } + + @Test + void chunkRestoreDependencyBindsWhenGeneratedSupportRowExists() { + PhysicsChunkLifecycle.enable(); + FakePhysicsBackendRuntimeProvider provider = + new FakePhysicsBackendRuntimeProvider(BACKEND_ID, false, false); + ImpulseBackendRegistry.registerRuntimeProvider(provider); + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + PhysicsChunkStoreTypes.registerPhysicsStoreResourceTypes(proxy); + proxy.registerSystem(new PersistenceHydrationSystem()); + proxy.registerSystem(new IdentityIndexSystem()); + proxy.registerSystem(new SpaceBindingSystem()); + proxy.registerSystem(new SpaceSettingsApplicationSystem()); + proxy.registerSystem(new BodyBindingSystem()); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("body-binding-restore-support-test")), + EmptyResourceStorage.get()); + try { + UUID spaceUuid = uuid(1); + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + restore.markComplete(); + restore.markHydrated(); + Ref spaceRef = addSpace(store, spaceUuid); + store.getResource(PhysicsChunkSettingsIndexResource.getResourceType()) + .replaceAll(Map.of(spaceUuid, + new PhysicsChunkSpaceSettings(spaceUuid, + PhysicsChunkCollisionMode.STREAMING, + EntityChunkBoundaryMode.PAUSE_UNTIL_LOADED, + false, + 8, + 4, + 100))); + Ref waitingBodyRef = addBody(store, uuid(2), spaceUuid, spaceRef); + store.putComponent(waitingBodyRef, + ChunkCollisionRestoreDependencyComponent.getComponentType(), + new ChunkCollisionRestoreDependencyComponent(spaceUuid, + new Vector3f(1.0f, 2.0f, 3.0f), + 4, + PhysicsChunkCollisionMode.STREAMING)); + Ref generatedBodyRef = addGeneratedChunkBody(store, + spaceUuid, + spaceRef); + + store.tick(0.0f); + + assertFalse(restore.isFailed(), restore.getFailureMessage()); + assertEquals(0, restore.getSoftSkipsByReason().size()); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + assertNotNull(runtime.getBodyHandle(waitingBodyRef)); + assertNotNull(runtime.getBodyHandle(generatedBodyRef)); + BackendSpaceHandle spaceHandle = runtime.getSpaceHandle(spaceRef); + assertNotNull(spaceHandle); + assertEquals(2, provider.createdRuntimes().get(0).bodyCount(spaceHandle.value())); + } finally { + if (!store.isShutdown()) { + registry.removeStore(store); + } + registry.shutdown(); + PhysicsChunkStoreTypes.clearPhysicsStoreResourceTypes(); + PhysicsChunkLifecycle.disable(); + } + } + + private static Ref addSpace(Store store, UUID spaceUuid) { + Ref spaceRef = store.addEntity(PhysicsEntities.spaceHolder(store, + spaceUuid, + new SpaceComponent(BACKEND_ID, new Vector3f(0.0f, -9.81f, 0.0f))), + AddReason.SPAWN); + assertNotNull(spaceRef); + return spaceRef; + } + + private static Ref addBody(Store store, + UUID bodyUuid, + UUID spaceUuid, + Ref spaceRef) { + BodyComponent body = new BodyComponent(spaceUuid); + body.setSpaceRef(spaceRef); + TargetComponent target = new TargetComponent(); + target.setActive(true); + Ref bodyRef = store.addEntity(PhysicsEntities.bodyHolder(store, + bodyUuid, + body, + new DynamicsComponent(PhysicsBodyType.DYNAMIC, 1.0f, 0.0f, 0.0f, false), + target, + new ColliderComponent(new Vector3f(), new Quaternionf(), false), + new ShapeComponent(ShapeType.BOX, + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + PhysicsAxis.Y, + 0.0f, + ""), + new MaterialComponent(0.6f, 0.1f), + new CollisionFilterComponent(PhysicsCollisionFilters.DYNAMIC_BODY, + PhysicsCollisionFilters.ALL)), + AddReason.SPAWN); + assertNotNull(bodyRef); + return bodyRef; + } + + private static Ref addConfiguredBody(Store store, + UUID bodyUuid, + UUID spaceUuid, + Ref spaceRef) { + BodyComponent body = new BodyComponent(spaceUuid); + body.setSpaceRef(spaceRef); + TargetComponent target = new TargetComponent(); + target.setActive(true); + Ref bodyRef = store.addEntity(PhysicsEntities.bodyHolder(store, + bodyUuid, + body, + new DynamicsComponent(PhysicsBodyType.DYNAMIC, 2.0f, 0.2f, 0.3f, true), + target, + new ColliderComponent(new Vector3f(), new Quaternionf(), true), + new ShapeComponent(ShapeType.BOX, + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + PhysicsAxis.Y, + 0.0f, + ""), + new MaterialComponent(0.72f, 0.18f), + new CollisionFilterComponent(PhysicsCollisionFilters.TERRAIN, + PhysicsCollisionFilters.DYNAMIC_BODY)), + AddReason.SPAWN); + assertNotNull(bodyRef); + return bodyRef; + } + + private static Ref addGeneratedChunkBody(Store store, + UUID spaceUuid, + Ref spaceRef) { + String sourceKey = "chunk:0:0:0"; + UUID bodyUuid = ChunkCollisionMutationDrainSystem.chunkCollisionBodyUuid(spaceUuid, + sourceKey, + PartKind.BOX, + 0); + BodyComponent body = new BodyComponent(spaceUuid); + body.setSpaceRef(spaceRef); + TargetComponent target = new TargetComponent(); + target.setActive(true); + Ref bodyRef = store.addEntity(PhysicsEntities.bodyHolder(store, + bodyUuid, + body, + new DynamicsComponent(PhysicsBodyType.STATIC, 0.0f, 0.0f, 0.0f, false), + target, + new ColliderComponent(new Vector3f(), new Quaternionf(), false), + new ShapeComponent(ShapeType.BOX, + 8.0f, + 8.0f, + 8.0f, + 0.0f, + 0.0f, + PhysicsAxis.Y, + 0.0f, + ""), + new MaterialComponent(0.6f, 0.1f), + new CollisionFilterComponent(PhysicsCollisionFilters.TERRAIN, + PhysicsCollisionFilters.ALL)), + AddReason.SPAWN); + assertNotNull(bodyRef); + store.putComponent(bodyRef, + ChunkCollisionSourceComponent.getComponentType(), + new ChunkCollisionSourceComponent(sourceKey, + 0, + 0, + 0, + "", + PartKind.BOX, + 0)); + return bodyRef; + } + + private static UUID uuid(long leastSignificantBits) { + return new UUID(0L, leastSignificantBits); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/BodyCommandApplicationSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/BodyCommandApplicationSystemTest.java new file mode 100644 index 00000000..160288e0 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/BodyCommandApplicationSystemTest.java @@ -0,0 +1,88 @@ +package dev.hytalemodding.impulse.core.internal.systems; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkStoreTypes; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.systems.binding.BodyBindingSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.SpaceBindingSystem; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.components.BodyCommandComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import java.util.ArrayList; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +class BodyCommandApplicationSystemTest { + + @Test + void copiedComponentCommandsMutateThroughCommandBufferDuringStoreTick() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + PhysicsChunkStoreTypes.registerPhysicsStoreResourceTypes(proxy); + proxy.registerSystem(new PersistenceHydrationSystem()); + proxy.registerSystem(new IdentityIndexSystem()); + proxy.registerSystem(new SpaceBindingSystem()); + proxy.registerSystem(new SpaceSettingsApplicationSystem()); + proxy.registerSystem(new BodyBindingSystem()); + proxy.registerSystem(new BodyCommandApplicationSystem()); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("body-command-application-test")), + EmptyResourceStorage.get()); + try { + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + restore.markComplete(); + restore.markHydrated(); + Ref bodyRef = addCommandedBody(store); + + store.tick(0.0f); + + DynamicsComponent dynamics = store.getComponent(bodyRef, + DynamicsComponent.getComponentType()); + assertNotNull(dynamics); + assertEquals(PhysicsBodyType.KINEMATIC, dynamics.getBodyType()); + CollisionFilterComponent filter = store.getComponent(bodyRef, + CollisionFilterComponent.getComponentType()); + assertNotNull(filter); + assertEquals(0x40, filter.getCollisionGroup()); + assertEquals(0x07, filter.getCollisionMask()); + assertNull(store.getComponent(bodyRef, BodyCommandComponent.getComponentType())); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Nonnull + private static Ref addCommandedBody(@Nonnull Store store) { + Holder holder = PhysicsEntities.entityHolder(store, UUID.randomUUID()); + holder.addComponent(DynamicsComponent.getComponentType(), + new DynamicsComponent(PhysicsBodyType.DYNAMIC, 1.0f, 0.0f, 0.0f, false)); + holder.addComponent(CollisionFilterComponent.getComponentType(), + new CollisionFilterComponent(0x01, 0x02)); + holder.addComponent(BodyCommandComponent.getComponentType(), + BodyCommandComponent.setType(PhysicsBodyType.KINEMATIC, false) + .append(BodyCommandComponent.setCollisionFilter(0x40, 0x07, false))); + return store.addEntity(holder, AddReason.SPAWN); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/CompletedStepPublicationSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/CompletedStepPublicationSystemTest.java new file mode 100644 index 00000000..b71b7ac5 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/CompletedStepPublicationSystemTest.java @@ -0,0 +1,131 @@ +package dev.hytalemodding.impulse.core.internal.systems; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.PhysicsStepPhaseStats; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsEventResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsProfilingResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStepSchedulerResource; +import dev.hytalemodding.impulse.core.internal.systems.publication.CompletedStepPublicationSystem; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsStepEvent; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepSchedulingMode; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import java.util.ArrayList; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; +import org.junit.jupiter.api.Test; + +class CompletedStepPublicationSystemTest { + + @Test + void completedStepPublishesCompactSnapshotAndRecordsCounts() throws Exception { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("completed-step-publication-compact")), + EmptyResourceStorage.get()); + try { + PhysicsStepSchedulerResource scheduler = store.getResource( + PhysicsStepSchedulerResource.getResourceType()); + PhysicsStepSchedulerResource.StepInput input = scheduler.acceptStepInput(0.05f, + PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT, + 0.10f); + UUID spaceUuid = uuid(1); + UUID firstBodyUuid = uuid(101); + UUID secondBodyUuid = uuid(102); + PhysicsSnapshotResource.CompactSnapshotBuilder builder = + PhysicsSnapshotResource.compactBuilder(2); + addBody(builder, firstBodyUuid, spaceUuid, PhysicsBodyType.DYNAMIC, 1.0f); + addBody(builder, secondBodyUuid, spaceUuid, PhysicsBodyType.STATIC, 2.0f); + PhysicsSnapshotResource.CompactSnapshot compactSnapshot = builder.build(); + + assertTrue(scheduler.submitStep(input, + () -> new PhysicsStepSchedulerResource.CompletedStep(3, + 9, + 123L, + 456L, + PhysicsStepPhaseStats.unavailable(), + compactSnapshot), + 10L)); + scheduler.whenIdle().toCompletableFuture().get(5, TimeUnit.SECONDS); + + new CompletedStepPublicationSystem().tick(0.25f, 0, store); + + PhysicsSnapshotResource snapshot = store.getResource( + PhysicsSnapshotResource.getResourceType()); + assertEquals(1L, snapshot.latestSequence()); + assertEquals(2, snapshot.bodyCount()); + assertEquals(0.05f, snapshot.getLatestFrame().dt(), 0.0001f); + PhysicsBodySnapshot firstBody = snapshot.getBody(firstBodyUuid); + assertNotNull(firstBody); + assertEquals(1.0f, firstBody.positionX(), 0.0001f); + + PhysicsProfilingResource.StepSample profiling = store.getResource( + PhysicsProfilingResource.getResourceType()).latestStepSample(); + assertEquals(123L, profiling.stepSubmitNanos()); + assertEquals(456L, profiling.snapshotNanos()); + assertEquals(2, profiling.publishedBodies()); + assertEquals(1, profiling.schedulerSamples()); + assertEquals(0.05f, profiling.schedulerSubmittedDtSeconds(), 0.0001f); + + PhysicsEventFrame eventFrame = store.getResource(PhysicsEventResource.getResourceType()) + .getLatestFrame(); + PhysicsStepEvent stepEvent = eventFrame.latestStep(); + assertNotNull(stepEvent); + assertEquals(1L, stepEvent.stepSequence()); + assertEquals(1L, stepEvent.snapshotFrameEpoch()); + assertEquals(2, stepEvent.bodyCount()); + assertEquals(123L, stepEvent.stepNanos()); + assertEquals(456L, stepEvent.snapshotNanos()); + } finally { + store.getResource(PhysicsStepSchedulerResource.getResourceType()).close(); + registry.removeStore(store); + registry.shutdown(); + } + } + + private static void addBody(@Nonnull PhysicsSnapshotResource.CompactSnapshotBuilder builder, + @Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid, + @Nonnull PhysicsBodyType bodyType, + float positionX) { + builder.addBody(null, + bodyUuid, + spaceUuid, + bodyType, + positionX, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + false); + } + + private static UUID uuid(long lowBits) { + return new UUID(0L, lowBits); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/JointBindingSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/JointBindingSystemTest.java new file mode 100644 index 00000000..ac1c5351 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/JointBindingSystemTest.java @@ -0,0 +1,235 @@ +package dev.hytalemodding.impulse.core.internal.systems; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.PhysicsAxis; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.FakePhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.modules.physicschunk.PhysicsChunkStoreTypes; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.systems.binding.BodyBindingSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.ColliderBindingSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.JointBindingSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.SpaceBindingSystem; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointType; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import java.util.ArrayList; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class JointBindingSystemTest { + + @Test + void jointBindingRejectsEndpointBodiesFromDifferentBackendWithSameSpaceHandle() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + PhysicsChunkStoreTypes.registerPhysicsStoreResourceTypes(proxy); + proxy.registerSystem(new PersistenceHydrationSystem()); + proxy.registerSystem(new IdentityIndexSystem()); + proxy.registerSystem(new SpaceBindingSystem()); + proxy.registerSystem(new SpaceSettingsApplicationSystem()); + proxy.registerSystem(new BodyBindingSystem()); + proxy.registerSystem(new ColliderBindingSystem()); + proxy.registerSystem(new JointBindingSystem()); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("joint-binding-cross-backend-test")), + EmptyResourceStorage.get()); + try { + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + restore.markComplete(); + restore.markHydrated(); + BoundSpace jointSpace = addBoundSpace(store, uuid(1), new BackendId("test:joint-a")); + BoundSpace bodySpace = addBoundSpace(store, uuid(2), new BackendId("test:joint-b")); + UUID bodyAUuid = uuid(3); + UUID bodyBUuid = uuid(4); + Ref bodyARef = addBoundBody(store, bodySpace, bodyAUuid, 0.0f); + Ref bodyBRef = addBoundBody(store, bodySpace, bodyBUuid, 1.0f); + UUID jointUuid = uuid(5); + Ref jointRef = addJoint(store, + jointSpace.uuid(), + jointSpace.ref(), + bodyAUuid, + bodyARef, + bodyBUuid, + bodyBRef, + jointUuid); + + store.tick(0.0f); + + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + assertFalse(restore.isFailed()); + assertNull(runtime.getJointHandle(jointRef)); + assertEquals(0, jointSpace.runtime().jointCount(jointSpace.handle().value())); + assertEquals(1, + restore.getSoftSkipsByReason() + .getInt("Joint endpoints are not in the joint space: " + jointUuid)); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Nonnull + private static BoundSpace addBoundSpace(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull BackendId backendId) { + Ref spaceRef = store.addEntity(PhysicsEntities.spaceHolder(store, + spaceUuid, + new SpaceComponent(backendId, new Vector3f(0.0f, -9.81f, 0.0f))), + AddReason.SPAWN); + assertNotNull(spaceRef); + store.getExternalData().putRefForUUID(spaceUuid, spaceRef); + FakePhysicsBackendRuntime runtime = (FakePhysicsBackendRuntime) + new FakePhysicsBackendRuntimeProvider(backendId, false, false).createRuntime(); + BackendSpaceHandle spaceHandle = new BackendSpaceHandle(runtime.createSpace(new SpaceId(42))); + PhysicsRuntimeResource runtimeResource = store.getResource( + PhysicsRuntimeResource.getResourceType()); + runtimeResource.putRuntime(backendId, runtime); + runtimeResource.putSpaceHandle(spaceRef, backendId, spaceHandle); + runtimeResource.putSpaceMetadata(backendId, spaceHandle, spaceUuid, spaceRef); + return new BoundSpace(spaceUuid, backendId, spaceRef, spaceHandle, runtime); + } + + @Nonnull + private static Ref addBoundBody(@Nonnull Store store, + @Nonnull BoundSpace space, + @Nonnull UUID bodyUuid, + float positionX) { + BodyComponent body = new BodyComponent(space.uuid()); + body.setSpaceRef(space.ref()); + Ref bodyRef = store.addEntity(PhysicsEntities.bodyHolder(store, + bodyUuid, + body, + new DynamicsComponent(PhysicsBodyType.DYNAMIC, 1.0f, 0.0f, 0.0f, false), + null, + new ColliderComponent(new Vector3f(), new Quaternionf(), false), + new ShapeComponent(ShapeType.BOX, + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + PhysicsAxis.Y, + 0.0f, + ""), + new MaterialComponent(0.5f, 0.1f), + new CollisionFilterComponent(0x01, 0x02)), + AddReason.SPAWN); + assertNotNull(bodyRef); + store.getExternalData().putRefForUUID(bodyUuid, bodyRef); + long bodyId = space.runtime().createBody(space.handle().value(), + BackendRuntimeCodes.shapeTypeCode(ShapeType.BOX), + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + BackendRuntimeCodes.axisCode(PhysicsAxis.Y), + 0.0f, + 1.0f, + BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC), + positionX, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + BackendBodyHandle bodyHandle = new BackendBodyHandle(bodyId); + PhysicsRuntimeResource runtimeResource = store.getResource( + PhysicsRuntimeResource.getResourceType()); + runtimeResource.putBodyHandle(bodyRef, space.ref(), space.handle(), bodyHandle); + runtimeResource.putBodySnapshotMetadata(space.backendId(), + space.handle(), + bodyHandle, + bodyUuid, + bodyRef, + space.uuid()); + runtimeResource.putBodyHitMetadata(space.backendId(), + space.handle(), + bodyHandle, + bodyUuid, + bodyRef, + PhysicsBodyType.DYNAMIC, + ShapeType.BOX); + return bodyRef; + } + + @Nonnull + private static Ref addJoint(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull Ref spaceRef, + @Nonnull UUID bodyAUuid, + @Nonnull Ref bodyARef, + @Nonnull UUID bodyBUuid, + @Nonnull Ref bodyBRef, + @Nonnull UUID jointUuid) { + JointComponent joint = new JointComponent(); + joint.setSpaceUuid(spaceUuid); + joint.setSpaceRef(spaceRef); + joint.setBodyAUuid(bodyAUuid); + joint.setBodyARef(bodyARef); + joint.setBodyBUuid(bodyBUuid); + joint.setBodyBRef(bodyBRef); + joint.setType(JointType.FIXED); + joint.setAnchorA(new Vector3f()); + joint.setAnchorB(new Vector3f()); + joint.setAxis(new Vector3f(0.0f, 1.0f, 0.0f)); + joint.setEnabled(true); + Ref jointRef = store.addEntity(PhysicsEntities.jointHolder(store, + jointUuid, + joint), + AddReason.SPAWN); + assertNotNull(jointRef); + store.getExternalData().putRefForUUID(jointUuid, jointRef); + return jointRef; + } + + @Nonnull + private static UUID uuid(long leastSignificantBits) { + return new UUID(0L, leastSignificantBits); + } + + private record BoundSpace(@Nonnull UUID uuid, + @Nonnull BackendId backendId, + @Nonnull Ref ref, + @Nonnull BackendSpaceHandle handle, + @Nonnull FakePhysicsBackendRuntime runtime) { + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/StaleBodyRemovalSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/StaleBodyRemovalSystemTest.java new file mode 100644 index 00000000..724bd25f --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/StaleBodyRemovalSystemTest.java @@ -0,0 +1,337 @@ +package dev.hytalemodding.impulse.core.internal.systems; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.RemoveReason; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.thread.TickingThread; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.PhysicsAxis; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsSnapshotFrame; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class StaleBodyRemovalSystemTest { + + @Test + void tickRemovesMultipleStaleBodiesAndTheirCopiedStateTogether() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("stale-body-removal-batch")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(1); + UUID firstStaleUuid = uuid(2); + UUID secondStaleUuid = uuid(3); + UUID retainedUuid = uuid(4); + BoundSpace space = addBoundSpace(store, + spaceUuid, + new BackendId("test:stale-body-removal")); + Ref firstStaleRef = addBody(store, spaceUuid, space.ref(), firstStaleUuid); + Ref secondStaleRef = addBody(store, + spaceUuid, + space.ref(), + secondStaleUuid); + Ref retainedRef = addBody(store, spaceUuid, space.ref(), retainedUuid); + bindBody(store, space, firstStaleUuid, firstStaleRef, 0.0f); + bindBody(store, space, secondStaleUuid, secondStaleRef, 2.0f); + bindBody(store, space, retainedUuid, retainedRef, 4.0f); + publishCopiedState(store, + spaceUuid, + firstStaleUuid, + firstStaleRef, + secondStaleUuid, + secondStaleRef, + retainedUuid, + retainedRef); + store.removeComponent(firstStaleRef, BodyComponent.getComponentType()); + store.removeComponent(secondStaleRef, BodyComponent.getComponentType()); + + new StaleBodyRemovalSystem().tick(0.0f, 0, store); + + PhysicsRuntimeResource runtime = store.getResource( + PhysicsRuntimeResource.getResourceType()); + PhysicsSnapshotResource snapshots = + store.getResource(PhysicsSnapshotResource.getResourceType()); + assertFalse(store.getResource(PhysicsRestoreStatusResource.getResourceType()) + .isFailed()); + assertNull(store.getExternalData().getRefFromUUID(firstStaleUuid)); + assertNull(store.getExternalData().getRefFromUUID(secondStaleUuid)); + assertNotNull(store.getExternalData().getRefFromUUID(retainedUuid)); + assertNull(runtime.getBodyHandle(firstStaleRef)); + assertNull(runtime.getBodyHandle(secondStaleRef)); + assertNotNull(runtime.getBodyHandle(retainedRef)); + assertEquals(1, space.runtime().bodyCount(space.handle().value())); + assertNull(snapshots.getBody(firstStaleUuid)); + assertNull(snapshots.getBody(secondStaleUuid)); + assertNotNull(snapshots.getBody(retainedUuid)); + assertNull(PhysicsBodies.spaceId(store, firstStaleUuid)); + assertNull(PhysicsBodies.spaceId(store, secondStaleUuid)); + assertEquals(new SpaceId(42), PhysicsBodies.spaceId(store, retainedUuid)); + assertFalse(firstStaleRef.isValid()); + assertFalse(secondStaleRef.isValid()); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void tickRemovesBackendBodyAfterAuthoritativeBodyEntityIsRemoved() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("stale-body-removal-entity-row")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(11); + UUID bodyUuid = uuid(12); + BoundSpace space = addBoundSpace(store, + spaceUuid, + new BackendId("test:stale-body-row-removal")); + Ref bodyRef = addBody(store, spaceUuid, space.ref(), bodyUuid); + bindBody(store, space, bodyUuid, bodyRef, 0.0f); + publishCopiedState(store, spaceUuid, bodyUuid, bodyRef); + + store.removeEntity(bodyRef, store.getRegistry().newHolder(), RemoveReason.REMOVE); + new StaleBodyRemovalSystem().tick(0.0f, 0, store); + + PhysicsRuntimeResource runtime = store.getResource( + PhysicsRuntimeResource.getResourceType()); + PhysicsSnapshotResource snapshots = + store.getResource(PhysicsSnapshotResource.getResourceType()); + assertFalse(store.getResource(PhysicsRestoreStatusResource.getResourceType()) + .isFailed()); + assertNull(store.getExternalData().getRefFromUUID(bodyUuid)); + assertNull(runtime.getBodyHandle(bodyRef)); + assertEquals(0, space.runtime().bodyCount(space.handle().value())); + assertNull(snapshots.getBody(bodyUuid)); + assertNull(PhysicsBodies.spaceId(store, bodyUuid)); + assertFalse(bodyRef.isValid()); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Nonnull + private static BoundSpace addBoundSpace(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull BackendId backendId) { + Ref spaceRef = store.addEntity(PhysicsEntities.spaceHolder(store, + spaceUuid, + new SpaceComponent(backendId, new Vector3f(0.0f, -9.81f, 0.0f))), + AddReason.SPAWN); + assertNotNull(spaceRef); + store.getExternalData().putRefForUUID(spaceUuid, spaceRef); + PhysicsBackendRuntime runtime = + new FakePhysicsBackendRuntimeProvider(backendId, false, false).createRuntime(); + BackendSpaceHandle spaceHandle = + new BackendSpaceHandle(runtime.createSpace(new SpaceId(42))); + PhysicsRuntimeResource runtimeResource = store.getResource( + PhysicsRuntimeResource.getResourceType()); + runtimeResource.putRuntime(backendId, runtime); + runtimeResource.putSpaceHandle(spaceRef, backendId, spaceHandle); + runtimeResource.putSpaceMetadata(backendId, spaceHandle, spaceUuid, spaceRef); + store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .putSpace(new SpaceId(42), spaceUuid); + return new BoundSpace(spaceUuid, spaceRef, runtime, spaceHandle, backendId); + } + + @Nonnull + private static Ref addBody(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull Ref spaceRef, + @Nonnull UUID bodyUuid) { + BodyComponent body = new BodyComponent(spaceUuid); + body.setSpaceRef(spaceRef); + Ref bodyRef = store.addEntity(PhysicsEntities.bodyHolder(store, + bodyUuid, + body, + new DynamicsComponent(PhysicsBodyType.DYNAMIC, 1.0f, 0.0f, 0.0f, false), + null, + new ColliderComponent(new Vector3f(), new Quaternionf(), false), + new ShapeComponent(ShapeType.BOX, + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + PhysicsAxis.Y, + 0.0f, + ""), + new MaterialComponent(0.5f, 0.1f), + new CollisionFilterComponent(0x01, 0x02)), + AddReason.SPAWN); + assertNotNull(bodyRef); + store.getExternalData().putRefForUUID(bodyUuid, bodyRef); + return bodyRef; + } + + private static void bindBody(@Nonnull Store store, + @Nonnull BoundSpace space, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef, + float positionX) { + long bodyId = space.runtime().createBody(space.handle().value(), + BackendRuntimeCodes.shapeTypeCode(ShapeType.BOX), + 0.5f, + 0.5f, + 0.5f, + 0.0f, + 0.0f, + BackendRuntimeCodes.axisCode(PhysicsAxis.Y), + 0.0f, + 1.0f, + BackendRuntimeCodes.bodyTypeCode(PhysicsBodyType.DYNAMIC), + positionX, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f); + BackendBodyHandle handle = new BackendBodyHandle(bodyId); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + runtime.putBodyHandle(bodyRef, space.ref(), space.handle(), handle); + runtime.putBodySnapshotMetadata(space.backendId(), + space.handle(), + handle, + bodyUuid, + bodyRef, + space.uuid()); + runtime.putBodyHitMetadata(space.backendId(), + space.handle(), + handle, + bodyUuid, + bodyRef, + PhysicsBodyType.DYNAMIC, + ShapeType.BOX); + } + + private static void publishCopiedState(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull UUID firstBodyUuid, + @Nonnull Ref firstBodyRef, + @Nonnull UUID secondBodyUuid, + @Nonnull Ref secondBodyRef, + @Nonnull UUID retainedBodyUuid, + @Nonnull Ref retainedBodyRef) { + store.getResource(PhysicsSnapshotResource.getResourceType()) + .publish(new PhysicsSnapshotFrame(1L, + 0.05f, + List.of(snapshot(firstBodyRef, firstBodyUuid, spaceUuid), + snapshot(secondBodyRef, secondBodyUuid, spaceUuid), + snapshot(retainedBodyRef, retainedBodyUuid, spaceUuid)))); + } + + private static void publishCopiedState(@Nonnull Store store, + @Nonnull UUID spaceUuid, + @Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef) { + store.getResource(PhysicsSnapshotResource.getResourceType()) + .publish(new PhysicsSnapshotFrame(1L, + 0.05f, + List.of(snapshot(bodyRef, bodyUuid, spaceUuid)))); + } + + @Nonnull + private static PhysicsBodySnapshot snapshot(@Nonnull Ref bodyRef, + @Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid) { + return PhysicsBodySnapshot.of(bodyRef, + bodyUuid, + spaceUuid, + PhysicsBodyType.DYNAMIC, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + 0.0f, + false); + } + + @Nonnull + private static UUID uuid(long leastSignificantBits) { + return new UUID(0L, leastSignificantBits); + } + + private static void markCurrentThreadAsWorldThread(@Nonnull Store store) { + try { + Method setThread = TickingThread.class.getDeclaredMethod("setThread", Thread.class); + setThread.setAccessible(true); + setThread.invoke(store.getExternalData().getWorld(), Thread.currentThread()); + } catch (NoSuchMethodException | IllegalAccessException exception) { + throw new AssertionError("Could not mark test world thread", exception); + } catch (InvocationTargetException exception) { + throw new AssertionError("Could not mark test world thread", + exception.getTargetException()); + } + } + + private record BoundSpace(@Nonnull UUID uuid, + @Nonnull Ref ref, + @Nonnull PhysicsBackendRuntime runtime, + @Nonnull BackendSpaceHandle handle, + @Nonnull BackendId backendId) { + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/StepSubmissionSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/StepSubmissionSystemTest.java new file mode 100644 index 00000000..5f6c1594 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/StepSubmissionSystemTest.java @@ -0,0 +1,49 @@ +package dev.hytalemodding.impulse.core.internal.systems; + +import static org.junit.jupiter.api.Assertions.assertNull; + +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsStepSchedulerResource; +import dev.hytalemodding.impulse.core.internal.systems.step.StepSubmissionSystem; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.Test; + +class StepSubmissionSystemTest { + + @Test + void tickWithNoRuntimeSpacesDoesNotSubmitOwnerLaneStep() throws Exception { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("step-submission-no-spaces")), + EmptyResourceStorage.get()); + try { + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + restore.markComplete(); + restore.markHydrated(); + + new StepSubmissionSystem().tick(0.05f, 0, store); + + PhysicsStepSchedulerResource scheduler = store.getResource( + PhysicsStepSchedulerResource.getResourceType()); + scheduler.whenIdle().toCompletableFuture().get(5, TimeUnit.SECONDS); + assertNull(scheduler.pollCompletedStep(), + "Zero-space physics ticks should not submit an owner-lane step"); + } finally { + store.getResource(PhysicsStepSchedulerResource.getResourceType()).close(); + registry.removeStore(store); + registry.shutdown(); + } + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/binding/SpaceBindingSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/binding/SpaceBindingSystemTest.java new file mode 100644 index 00000000..0fe582dd --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/binding/SpaceBindingSystemTest.java @@ -0,0 +1,139 @@ +package dev.hytalemodding.impulse.core.internal.systems.binding; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.FakePhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldSettingsResource; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.UUID; +import java.util.function.BiConsumer; +import javax.annotation.Nonnull; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class SpaceBindingSystemTest { + + @Test + void boundSpaceBackendIdMutationFailsRestoreInsteadOfSilentlyKeepingOldBinding() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("space-backend-mutation-test")), + EmptyResourceStorage.get()); + try { + UUID spaceUuid = UUID.fromString("00000000-0000-0000-0000-000000000071"); + BackendId originalBackendId = new BackendId("test:space-backend-original"); + BackendId mutatedBackendId = new BackendId("test:space-backend-mutated"); + Ref spaceRef = store.addEntity(PhysicsEntities.spaceHolder(store, + spaceUuid, + new SpaceComponent(originalBackendId, new Vector3f(0.0f, -9.81f, 0.0f))), + AddReason.SPAWN); + assertNotNull(spaceRef); + store.getExternalData().putRefForUUID(spaceUuid, spaceRef); + FakePhysicsBackendRuntime backendRuntime = (FakePhysicsBackendRuntime) + new FakePhysicsBackendRuntimeProvider(originalBackendId, false, false) + .createRuntime(); + BackendSpaceHandle spaceHandle = + new BackendSpaceHandle(backendRuntime.createSpace(new SpaceId(72))); + PhysicsRuntimeResource runtime = store.getResource( + PhysicsRuntimeResource.getResourceType()); + runtime.putRuntime(originalBackendId, backendRuntime); + runtime.putSpaceHandle(spaceRef, originalBackendId, spaceHandle); + runtime.putSpaceMetadata(originalBackendId, spaceHandle, spaceUuid, spaceRef); + + SpaceComponent component = store.getComponent(spaceRef, SpaceComponent.getComponentType()); + assertNotNull(component); + component.setBackendId(mutatedBackendId); + + runSpaceBindingSystem(store); + + PhysicsRestoreStatusResource restore = + store.getResource(PhysicsRestoreStatusResource.getResourceType()); + assertTrue(restore.isFailed()); + assertEquals(originalBackendId, runtime.getSpaceBackendId(spaceRef)); + assertEquals(spaceHandle, runtime.getSpaceHandle(spaceRef)); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + private static void runSpaceBindingSystem(@Nonnull Store store) { + try { + SpaceBindingSystem system = new SpaceBindingSystem(); + Method bindChunk = SpaceBindingSystem.class.getDeclaredMethod("bindChunk", + PhysicsRuntimeResource.class, + PhysicsSpaceCompatibilityIndexResource.class, + PhysicsRestoreStatusResource.class, + PhysicsStepMode.class, + ArchetypeChunk.class); + bindChunk.setAccessible(true); + PhysicsRuntimeResource runtime = store.getResource( + PhysicsRuntimeResource.getResourceType()); + PhysicsSpaceCompatibilityIndexResource compatibility = store.getResource( + PhysicsSpaceCompatibilityIndexResource.getResourceType()); + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + PhysicsStepMode stepMode = + store.getResource(PhysicsWorldSettingsResource.getResourceType()) + .getSettings() + .getStepMode(); + BiConsumer, CommandBuffer> collector = + (chunk, _) -> invoke(bindChunk, + runtime, + compatibility, + restore, + stepMode, + chunk); + store.forEachChunk(system.getQuery(), collector); + } catch (NoSuchMethodException exception) { + throw new AssertionError("Could not run SpaceBindingSystem", exception); + } + } + + private static void invoke(@Nonnull Method method, @Nonnull Object... arguments) { + try { + method.invoke(null, arguments); + } catch (IllegalAccessException exception) { + throw new AssertionError(exception); + } catch (InvocationTargetException exception) { + Throwable cause = exception.getCause(); + if (cause instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (cause instanceof Error error) { + throw error; + } + throw new AssertionError(cause); + } + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/body/PhysicsBodyIdentityCleanupSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/body/PhysicsBodyIdentityCleanupSystemTest.java deleted file mode 100644 index a4335307..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/body/PhysicsBodyIdentityCleanupSystemTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.body; - -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; - -import com.hypixel.hytale.component.ComponentRegistry; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.EmptyResourceStorage; -import com.hypixel.hytale.component.ResourceType; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyIdentityComponent; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import org.junit.jupiter.api.Test; - -class PhysicsBodyIdentityCleanupSystemTest { - - @Test - void removingEntityAuthoredIdentityDestroysBackendBody() { - FakePhysicsBackend backend = new FakePhysicsBackend("test:identity-cleanup"); - Impulse.registerBackend(backend); - ComponentRegistry registry = new ComponentRegistry<>(); - ComponentType identityType = - registry.registerComponent(PhysicsBodyIdentityComponent.class, - "PhysicsBodyIdentity", - PhysicsBodyIdentityComponent.CODEC); - ResourceType resourceType = - registry.registerResource(PhysicsWorldResource.class, - LegacyLiveHandleTestResource::new); - Store store = registry.addStore( - new EntityStore(TestInstanceFactory.world("identity-cleanup-test")), - EmptyResourceStorage.get()); - try { - LegacyLiveHandleTestResource resource = - (LegacyLiveHandleTestResource) store.getResource(resourceType); - PhysicsSpace space = resource.createLiveSpace(backend.getId()); - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - RigidBodyKey bodyKey = RigidBodyKey.random(); - resource.addBody(bodyKey, - space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - - new PhysicsBodyIdentityCleanupSystem(identityType, resourceType).onComponentRemoved(null, - new PhysicsBodyIdentityComponent(bodyKey, - space.id(), - PhysicsBodyPersistenceMode.PERSISTENT), - store, - null); - - assertNull(resource.getBody(bodyKey)); - } finally { - registry.removeStore(store); - registry.shutdown(); - } - } - - @Test - void replacingEntityAuthoredIdentityDestroysOldBackendBodyOnly() { - FakePhysicsBackend backend = new FakePhysicsBackend("test:identity-replacement-cleanup"); - Impulse.registerBackend(backend); - ComponentRegistry registry = new ComponentRegistry<>(); - ComponentType identityType = - registry.registerComponent(PhysicsBodyIdentityComponent.class, - "PhysicsBodyIdentity", - PhysicsBodyIdentityComponent.CODEC); - ResourceType resourceType = - registry.registerResource(PhysicsWorldResource.class, - LegacyLiveHandleTestResource::new); - Store store = registry.addStore( - new EntityStore(TestInstanceFactory.world("identity-replacement-cleanup-test")), - EmptyResourceStorage.get()); - try { - LegacyLiveHandleTestResource resource = - (LegacyLiveHandleTestResource) store.getResource(resourceType); - PhysicsSpace space = resource.createLiveSpace(backend.getId()); - RigidBodyKey oldKey = RigidBodyKey.random(); - RigidBodyKey newKey = RigidBodyKey.random(); - resource.addBody(oldKey, - space.id(), - space.createBox(0.5f, 0.5f, 0.5f, 1.0f), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - resource.addBody(newKey, - space.id(), - space.createBox(0.5f, 0.5f, 0.5f, 1.0f), - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - - new PhysicsBodyIdentityCleanupSystem(identityType, resourceType).onComponentSet(null, - new PhysicsBodyIdentityComponent(oldKey, - space.id(), - PhysicsBodyPersistenceMode.PERSISTENT), - new PhysicsBodyIdentityComponent(newKey, - space.id(), - PhysicsBodyPersistenceMode.PERSISTENT), - store, - null); - - assertNull(resource.getBody(oldKey)); - assertNotNull(resource.getBody(newKey)); - } finally { - registry.removeStore(store); - registry.shutdown(); - } - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyCommandBatchTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyCommandBatchTest.java deleted file mode 100644 index 1bed478c..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodyCommandBatchTest.java +++ /dev/null @@ -1,100 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.body; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.simulation.recorder.MutablePhysicsCommandContext; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyCollisionComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyDynamicsComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyIdentityComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyKinematicTargetComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyMaterialComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyShapeComponent; -import org.joml.Quaternionf; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class RigidBodyCommandBatchTest { - - @Test - void recordsMultipleSpawnsAndTargetsIntoOneCommandContext() { - RigidBodyKey firstKey = RigidBodyKey.of(0L, 101L); - RigidBodyKey secondKey = RigidBodyKey.of(0L, 102L); - RigidBodyCommandBatch batch = new RigidBodyCommandBatch(); - - batch.addSpawn(spawnPlan(firstKey), new Vector3f(1.0f, 2.0f, 3.0f)); - batch.addSpawn(spawnPlan(secondKey), new Vector3f(4.0f, 5.0f, 6.0f)); - batch.addKinematicTarget(firstKey, target(7.0f, true, true)); - - assertTrue(batch.hasPendingBody(firstKey)); - assertTrue(batch.hasPendingBody(secondKey)); - assertEquals(4, batch.expectedOperations()); - - MutablePhysicsCommandContext context = new MutablePhysicsCommandContext(25L, - 9L, - batch.expectedOperations()); - batch.record(context); - - assertEquals(4, context.freezeInternal(31L).publicBatch().commandCount()); - } - - @Test - void kinematicTargetStateAcceptsOnlyChangedTargets() { - RigidBodyKey bodyKey = RigidBodyKey.of(0L, 103L); - RigidBodyKinematicTargetState state = new RigidBodyKinematicTargetState(); - PhysicsBodyKinematicTargetComponent target = target(1.0f, true, false); - - state.beginTick(); - assertTrue(state.shouldSubmit(bodyKey, target)); - state.finishTick(); - state.beginTick(); - assertFalse(state.shouldSubmit(bodyKey, target(1.0f, true, false))); - assertTrue(state.shouldSubmit(bodyKey, target(2.0f, true, false))); - state.finishTick(); - } - - @Test - void kinematicTargetStatePrunesTargetsThatAreNoLongerObserved() { - RigidBodyKey bodyKey = RigidBodyKey.of(0L, 104L); - RigidBodyKinematicTargetState state = new RigidBodyKinematicTargetState(); - - state.beginTick(); - assertTrue(state.shouldSubmit(bodyKey, target(1.0f, true, false))); - state.finishTick(); - - state.beginTick(); - state.finishTick(); - - assertEquals(0, state.trackedTargetCount()); - } - - private static RigidBodySpawnPlan spawnPlan(RigidBodyKey bodyKey) { - return RigidBodySpawnPlan.create( - new PhysicsBodyIdentityComponent(bodyKey, - new SpaceId(7), - PhysicsBodyPersistenceMode.RUNTIME_ONLY), - PhysicsBodyShapeComponent.box(0.5f, 0.5f, 0.5f), - new PhysicsBodyDynamicsComponent(PhysicsBodyType.DYNAMIC, 1.0f, 0.0f, 0.0f), - new PhysicsBodyMaterialComponent(), - new PhysicsBodyCollisionComponent()); - } - - private static PhysicsBodyKinematicTargetComponent target(float positionX, - boolean transformEnabled, - boolean velocityEnabled) { - return new PhysicsBodyKinematicTargetComponent( - new Vector3f(positionX, 2.0f, 3.0f), - new Quaternionf(), - new Vector3f(0.1f, 0.2f, 0.3f), - new Vector3f(0.4f, 0.5f, 0.6f), - transformEnabled, - velocityEnabled, - true); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodySpawnPlanTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodySpawnPlanTest.java deleted file mode 100644 index 47e092fe..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/body/RigidBodySpawnPlanTest.java +++ /dev/null @@ -1,127 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.body; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyCollisionComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyDynamicsComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyIdentityComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyLifecycleComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyMaterialComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyShapeComponent; -import org.junit.jupiter.api.Test; - -class RigidBodySpawnPlanTest { - - @Test - void entityAuthoredBodiesProduceSpawnPlansWithExplicitSpace() { - RigidBodyKey bodyKey = RigidBodyKey.of(0L, 81L); - - RigidBodySpawnPlan plan = RigidBodySpawnPlan.create( - identity(bodyKey, new SpaceId(3)), - shape(), - dynamics(), - material(), - collision()); - - assertEquals(bodyKey, plan.bodyKey()); - assertEquals(new SpaceId(3), plan.spaceId()); - assertEquals(PhysicsBodyType.DYNAMIC, plan.bodyType()); - assertEquals(2.0f, plan.mass(), 0.0001f); - assertEquals(PhysicsBodyPersistenceMode.PERSISTENT, plan.persistenceMode()); - } - - @Test - void missingExplicitSpaceFailsInsteadOfChoosingDefault() { - IllegalArgumentException failure = assertThrows(IllegalArgumentException.class, - () -> RigidBodySpawnPlan.create(identity(RigidBodyKey.of(0L, 84L), null), - shape(), - dynamics(), - material(), - collision())); - - assertEquals("PhysicsBodyIdentityComponent must hold a positive explicit SpaceId", - failure.getMessage()); - } - - @Test - void missingShapeFailsInsteadOfSilentlySkippingTheEntity() { - IllegalArgumentException failure = assertThrows(IllegalArgumentException.class, - () -> RigidBodySpawnPlan.create(identity(RigidBodyKey.of(0L, 82L), new SpaceId(4)), - null, - dynamics(), - material(), - collision())); - - assertEquals("PhysicsBodyShapeComponent is required", failure.getMessage()); - } - - @Test - void missingDynamicsFailsInsteadOfApplyingImplicitDefaults() { - IllegalArgumentException failure = assertThrows(IllegalArgumentException.class, - () -> RigidBodySpawnPlan.create(identity(RigidBodyKey.of(0L, 83L), new SpaceId(5)), - shape(), - null, - material(), - collision())); - - assertEquals("PhysicsBodyDynamicsComponent is required", failure.getMessage()); - } - - @Test - void destroyedLifecycleSuppressesEntityAuthoredRespawn() { - RigidBodyKey bodyKey = RigidBodyKey.of(0L, 85L); - PhysicsBodyLifecycleComponent lifecycle = PhysicsBodyLifecycleComponent.destroyed(bodyKey); - - assertTrue(RigidBodyReconciliationPolicy.shouldSuppressBodyReconciliation(lifecycle, - null, - bodyKey)); - } - - @Test - void skippedRestoreKeySuppressesEntityAuthoredRespawn() { - RigidBodyKey bodyKey = RigidBodyKey.of(0L, 86L); - PersistentPhysicsWorldResource persistent = new PersistentPhysicsWorldResource(); - persistent.recordRuntimeBodySkipped(bodyKey, "invalid position"); - - assertTrue(RigidBodyReconciliationPolicy.shouldSuppressBodyReconciliation(null, - persistent, - bodyKey)); - assertFalse(RigidBodyReconciliationPolicy.shouldSuppressBodyReconciliation(null, - persistent, - RigidBodyKey.of(0L, 87L))); - } - - private static PhysicsBodyIdentityComponent identity(RigidBodyKey bodyKey, SpaceId spaceId) { - return new PhysicsBodyIdentityComponent(bodyKey, - spaceId, - PhysicsBodyPersistenceMode.PERSISTENT); - } - - private static PhysicsBodyShapeComponent shape() { - return PhysicsBodyShapeComponent.capsule(0.75f, 0.5f, PhysicsAxis.Y); - } - - private static PhysicsBodyDynamicsComponent dynamics() { - return new PhysicsBodyDynamicsComponent(PhysicsBodyType.DYNAMIC, - 2.0f, - 0.0f, - 0.0f); - } - - private static PhysicsBodyMaterialComponent material() { - return new PhysicsBodyMaterialComponent(); - } - - private static PhysicsBodyCollisionComponent collision() { - return new PhysicsBodyCollisionComponent(); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsContactDebugCapture.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsContactDebugCapture.java index 2586661b..bb5e1918 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsContactDebugCapture.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsContactDebugCapture.java @@ -1,7 +1,5 @@ package dev.hytalemodding.impulse.core.internal.systems.debug; -import dev.hytalemodding.impulse.api.PhysicsContact; -import dev.hytalemodding.impulse.api.PhysicsSpace; import java.util.ArrayList; import java.util.List; import javax.annotation.Nonnull; @@ -15,7 +13,7 @@ private PhysicsContactDebugCapture() { @Nonnull static List collectVisibleContactPrimitives( - @Nonnull PhysicsSpace space, + @Nonnull List contacts, @Nonnull Vector3d center, double radius, int maxContacts) { @@ -24,7 +22,7 @@ static List collectVisibleContactPri } List primitives = new ArrayList<>(); double radiusSquared = radius * radius; - for (PhysicsContact contact : space.getContacts()) { + for (ContactDebugSource contact : contacts) { Vector3f pointOnB = contact.pointOnB(); Vector3d point = new Vector3d(pointOnB.x, pointOnB.y, pointOnB.z); if (point.distanceSquared(center) <= radiusSquared) { @@ -38,4 +36,7 @@ static List collectVisibleContactPri } return List.copyOf(primitives); } + + record ContactDebugSource(@Nonnull Vector3f pointOnB, @Nonnull Vector3f normalOnB) { + } } diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugSystemTest.java index 5a2d1a26..8155a7f3 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugSystemTest.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsDebugSystemTest.java @@ -4,21 +4,12 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import dev.hytalemodding.impulse.api.BackendId; import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBody; import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsContact; -import dev.hytalemodding.impulse.api.PhysicsSpace; import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend.InMemoryPhysicsSpace; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsDebugContactView; -import dev.hytalemodding.impulse.core.internal.simulation.view.PhysicsDebugJointView; import java.util.concurrent.CompletableFuture; import java.util.List; import java.util.UUID; @@ -62,8 +53,7 @@ void debugCenterInvertsSyncedAttachmentLocalPositionOffset() { Vector3d syncedVisualPosition = new Vector3d(snapshot.positionX(), snapshot.positionY() - snapshot.centerOfMassOffsetY(), snapshot.positionZ()).add(localOffset.x, localOffset.y, localOffset.z); - PhysicsBodyAttachmentComponent attachment = PhysicsBodyAttachmentComponent.externalEntity(RigidBodyKey.random(), - null, + BodyAttachmentComponent attachment = BodyAttachmentComponent.externalEntity(UUID.randomUUID(), localOffset, new Quaternionf()); @@ -112,8 +102,7 @@ void debugPoseUsesSyncedTransformRotationWhenSnapshotRotationIsStale() { syncedVisualPosition.add(syncedBodyRotation.transform(new Vector3d(localOffset.x, localOffset.y, localOffset.z))); - PhysicsBodyAttachmentComponent attachment = PhysicsBodyAttachmentComponent.externalEntity(RigidBodyKey.random(), - null, + BodyAttachmentComponent attachment = BodyAttachmentComponent.externalEntity(UUID.randomUUID(), localOffset, new Quaternionf()); @@ -159,8 +148,7 @@ void debugCenterUsesAttachmentVisualOriginOffset() { 0.0f, PhysicsAxis.Y); Vector3f localOffset = new Vector3f(0.0f, -0.5f, 0.0f); - PhysicsBodyAttachmentComponent attachment = PhysicsBodyAttachmentComponent.externalEntity(RigidBodyKey.random(), - null, + BodyAttachmentComponent attachment = BodyAttachmentComponent.externalEntity(UUID.randomUUID(), localOffset, new Quaternionf(), 0.5f); @@ -286,20 +274,16 @@ void debugQueryCacheDoesNotSubmitDuplicateJointsWhilePending() { @Test void collectVisibleJointPrimitivesUsesAnchorsForDistanceAndRendering() { - PhysicsSpace space = new FakePhysicsBackend(new BackendId("test:debug-joints")) - .createSpace(new SpaceId(1)); - PhysicsBody bodyA = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody bodyB = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - bodyA.setPosition(1.0f, 0.0f, 0.0f); - bodyB.setPosition(5.0f, 0.0f, 0.0f); - space.createHingeJoint(bodyA, - bodyB, - new Vector3f(1.0f, 0.0f, 0.0f), - new Vector3f(-1.0f, 0.0f, 0.0f), - new Vector3f(0.0f, 1.0f, 0.0f)); + List joints = List.of( + new PhysicsJointDebugCapture.JointDebugSource( + new Vector3f(1.0f, 0.0f, 0.0f), + new Vector3f(5.0f, 0.0f, 0.0f), + new Vector3f(1.0f, 0.0f, 0.0f), + new Vector3f(-1.0f, 0.0f, 0.0f), + new Vector3f(0.0f, 1.0f, 0.0f))); List visible = - PhysicsJointDebugCapture.collectVisibleJointPrimitives(space, + PhysicsJointDebugCapture.collectVisibleJointPrimitives(joints, new Vector3d(3.0, 0.0, 0.0), 1.0, 4); @@ -312,11 +296,11 @@ void collectVisibleJointPrimitivesUsesAnchorsForDistanceAndRendering() { assertEquals(0.9, primitive.axis().y, 0.00001); assertEquals(0.0, primitive.axis().z, 0.00001); - assertTrue(PhysicsJointDebugCapture.collectVisibleJointPrimitives(space, + assertTrue(PhysicsJointDebugCapture.collectVisibleJointPrimitives(joints, new Vector3d(8.0, 0.0, 0.0), 1.0, 4).isEmpty()); - assertTrue(PhysicsJointDebugCapture.collectVisibleJointPrimitives(space, + assertTrue(PhysicsJointDebugCapture.collectVisibleJointPrimitives(joints, new Vector3d(3.0, 0.0, 0.0), 1.0, 0).isEmpty()); @@ -324,20 +308,13 @@ void collectVisibleJointPrimitivesUsesAnchorsForDistanceAndRendering() { @Test void collectVisibleContactPrimitivesCapturesPointsAndNormals() { - InMemoryPhysicsSpace space = (InMemoryPhysicsSpace) new FakePhysicsBackend( - new BackendId("test:debug-contacts")).createSpace(new SpaceId(2)); - PhysicsBody bodyA = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody bodyB = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - space.addContact(new PhysicsContact(bodyA, - bodyB, - new Vector3f(0.0f, 0.0f, 0.0f), - new Vector3f(1.0f, 2.0f, 3.0f), - new Vector3f(0.0f, 2.0f, 0.0f), - -0.1f, - 20.0f)); + List contacts = List.of( + new PhysicsContactDebugCapture.ContactDebugSource( + new Vector3f(1.0f, 2.0f, 3.0f), + new Vector3f(0.0f, 2.0f, 0.0f))); List visible = - PhysicsContactDebugCapture.collectVisibleContactPrimitives(space, + PhysicsContactDebugCapture.collectVisibleContactPrimitives(contacts, new Vector3d(1.0, 2.0, 3.0), 0.5, 4); @@ -349,11 +326,11 @@ void collectVisibleContactPrimitivesCapturesPointsAndNormals() { assertEquals(1.0, primitive.normal().y, 0.00001); assertEquals(0.0, primitive.normal().z, 0.00001); - assertTrue(PhysicsContactDebugCapture.collectVisibleContactPrimitives(space, + assertTrue(PhysicsContactDebugCapture.collectVisibleContactPrimitives(contacts, new Vector3d(3.0, 2.0, 3.0), 0.5, 4).isEmpty()); - assertTrue(PhysicsContactDebugCapture.collectVisibleContactPrimitives(space, + assertTrue(PhysicsContactDebugCapture.collectVisibleContactPrimitives(contacts, new Vector3d(1.0, 2.0, 3.0), 0.5, 0).isEmpty()); diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsJointDebugCapture.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsJointDebugCapture.java index 956f74af..2a8241fe 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsJointDebugCapture.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsJointDebugCapture.java @@ -1,10 +1,9 @@ package dev.hytalemodding.impulse.core.internal.systems.debug; -import dev.hytalemodding.impulse.api.PhysicsJoint; -import dev.hytalemodding.impulse.api.PhysicsSpace; import java.util.ArrayList; import java.util.List; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.joml.Vector3d; import org.joml.Vector3f; @@ -15,7 +14,7 @@ private PhysicsJointDebugCapture() { @Nonnull static List collectVisibleJointPrimitives( - @Nonnull PhysicsSpace space, + @Nonnull List joints, @Nonnull Vector3d center, double radius, int maxJoints) { @@ -24,12 +23,12 @@ static List collectVisibleJointPrimiti } List primitives = new ArrayList<>(); double radiusSquared = radius * radius; - for (PhysicsJoint joint : space.getJoints()) { - Vector3d anchorA = worldAnchor(joint.getBodyA().getPosition(), joint.getAnchorA()); - Vector3d anchorB = worldAnchor(joint.getBodyB().getPosition(), joint.getAnchorB()); + for (JointDebugSource joint : joints) { + Vector3d anchorA = worldAnchor(joint.bodyAPosition(), joint.anchorA()); + Vector3d anchorB = worldAnchor(joint.bodyBPosition(), joint.anchorB()); Vector3d midpoint = new Vector3d(anchorA).add(anchorB).mul(0.5); if (midpoint.distanceSquared(center) <= radiusSquared) { - Vector3f axis = joint.getAxis(); + Vector3f axis = joint.axis(); Vector3d axisDebug = axis != null ? new Vector3d(axis.x, axis.y, axis.z).normalize().mul(0.9) : null; @@ -49,4 +48,11 @@ private static Vector3d worldAnchor(@Nonnull Vector3f bodyPosition, bodyPosition.y + localAnchor.y, bodyPosition.z + localAnchor.z); } + + record JointDebugSource(@Nonnull Vector3f bodyAPosition, + @Nonnull Vector3f bodyBPosition, + @Nonnull Vector3f anchorA, + @Nonnull Vector3f anchorB, + @Nullable Vector3f axis) { + } } diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsStoreDebugQueriesTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsStoreDebugQueriesTest.java new file mode 100644 index 00000000..0d26477a --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/debug/PhysicsStoreDebugQueriesTest.java @@ -0,0 +1,180 @@ +package dev.hytalemodding.impulse.core.internal.systems.debug; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.thread.TickingThread; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.api.runtime.BackendContactSink; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class PhysicsStoreDebugQueriesTest { + + @Test + void contactDebugQueryUsesBoundedRuntimeContactRead() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("debug-contact-query-bounded-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + RecordingContactRuntime runtime = new RecordingContactRuntime(); + bindSpace(store, runtime); + + List contacts = PhysicsStoreDebugQueries.contacts(store, + new SpaceId(77), + 0.0, + 0.0, + 0.0, + 32.0, + 3); + + assertEquals(3, contacts.size()); + assertEquals(1, runtime.boundedCalls()); + assertEquals(0, runtime.unboundedCalls()); + assertEquals(3, runtime.lastMaxContacts()); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + private static void bindSpace(@Nonnull Store store, + @Nonnull RecordingContactRuntime recordingRuntime) { + UUID spaceUuid = new UUID(0L, 77L); + BackendId backendId = new BackendId("test:debug-contact-query"); + Ref spaceRef = store.addEntity(PhysicsEntities.spaceHolder(store, + spaceUuid, + new SpaceComponent(backendId, new Vector3f(0.0f, -9.81f, 0.0f))), + AddReason.SPAWN); + store.getExternalData().putRefForUUID(spaceUuid, spaceRef); + store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .putSpace(new SpaceId(77), spaceUuid); + PhysicsRuntimeResource runtime = store.getResource(PhysicsRuntimeResource.getResourceType()); + BackendSpaceHandle spaceHandle = new BackendSpaceHandle(7700); + runtime.putRuntime(backendId, recordingRuntime.proxy()); + runtime.putSpaceHandle(spaceRef, backendId, spaceHandle); + runtime.putSpaceMetadata(backendId, spaceHandle, spaceUuid, spaceRef); + } + + private static void markCurrentThreadAsWorldThread(@Nonnull Store store) { + try { + Method setThread = TickingThread.class.getDeclaredMethod("setThread", Thread.class); + setThread.setAccessible(true); + setThread.invoke(store.getExternalData().getWorld(), Thread.currentThread()); + } catch (NoSuchMethodException | IllegalAccessException exception) { + throw new AssertionError("Could not mark test world thread", exception); + } catch (InvocationTargetException exception) { + throw new AssertionError("Could not mark test world thread", exception.getCause()); + } + } + + private static final class RecordingContactRuntime implements InvocationHandler { + + private final PhysicsBackendRuntime proxy = + (PhysicsBackendRuntime) Proxy.newProxyInstance( + PhysicsBackendRuntime.class.getClassLoader(), + new Class[] { PhysicsBackendRuntime.class }, + this); + private int boundedCalls; + private int unboundedCalls; + private int lastMaxContacts; + + @Nonnull + private PhysicsBackendRuntime proxy() { + return proxy; + } + + private int boundedCalls() { + return boundedCalls; + } + + private int unboundedCalls() { + return unboundedCalls; + } + + private int lastMaxContacts() { + return lastMaxContacts; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) { + if ("contacts".equals(method.getName()) && args != null && args.length == 3) { + boundedCalls++; + lastMaxContacts = (Integer) args[1]; + return emitContacts(lastMaxContacts, (BackendContactSink) args[2]); + } + if ("contacts".equals(method.getName()) && args != null && args.length == 2) { + unboundedCalls++; + return emitContacts(8, (BackendContactSink) args[1]); + } + return defaultValue(method.getReturnType()); + } + + private static int emitContacts(int maxContacts, @Nonnull BackendContactSink sink) { + int emitted = 0; + for (int i = 0; i < Math.max(0, maxContacts); i++) { + sink.accept(1L, + 2L, + i, + 0.0f, + 0.0f, + i, + 0.0f, + 0.0f, + 0.0f, + 1.0f, + 0.0f, + -0.1f, + 1.0f); + emitted++; + } + return emitted; + } + + private static Object defaultValue(@Nonnull Class returnType) { + if (returnType == Boolean.TYPE) { + return false; + } + if (returnType == Integer.TYPE) { + return 0; + } + if (returnType == Long.TYPE) { + return 0L; + } + if (returnType == Float.TYPE) { + return 0.0f; + } + return null; + } + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/owner/PhysicsOwnerLifecycleSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/owner/PhysicsOwnerLifecycleSystemTest.java deleted file mode 100644 index 05df0dca..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/owner/PhysicsOwnerLifecycleSystemTest.java +++ /dev/null @@ -1,392 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.owner; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertInstanceOf; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.hypixel.hytale.component.ComponentRegistry; -import com.hypixel.hytale.component.EmptyResourceStorage; -import com.hypixel.hytale.component.ResourceType; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.internal.resources.owner.TestPhysicsOwnerLane; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerHandle; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerLaneResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerLaneScheduler; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerCallable; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerCommand; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerMutation; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerMutationCompletion; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerResult; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerStepCompletion; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.junit.jupiter.api.Test; - -class PhysicsOwnerLifecycleSystemTest { - - @Test - void storeAddAndRemoveStartAndCloseOwnerResource() { - ComponentRegistry registry = new ComponentRegistry<>(); - ResourceType ownerLaneType = - registry.registerResource(TestPhysicsOwnerLane.class, - TestPhysicsOwnerLane::new); - ResourceType physicsType = - registry.registerResource(CountingPhysicsWorldResource.class, - CountingPhysicsWorldResource::new); - PhysicsOwnerLifecycleSystem lifecycle = - new PhysicsOwnerLifecycleSystem(ownerLaneType, physicsType); - registry.registerSystem(lifecycle); - - Store store = registry.addStore(testEntityStore("lifecycle-test"), - EmptyResourceStorage.get()); - TestPhysicsOwnerLane owner = store.getResource(ownerLaneType); - CountingPhysicsWorldResource physics = store.getResource(physicsType); - - assertTrue(owner.isStarted()); - assertEquals(1, lifecycle.activeOwnerCount()); - - registry.removeStore(store); - - assertTrue(owner.isClosed()); - assertEquals(1, physics.clearCalls); - assertEquals(0, lifecycle.activeOwnerCount()); - lifecycle.close(); - registry.shutdown(); - } - - @Test - void storeAddAndRemoveAttachInterfaceBackedOwnerResource() { - ComponentRegistry registry = new ComponentRegistry<>(); - ResourceType ownerType = - registry.registerResource(PhysicsOwnerResource.class, - TestPhysicsOwnerLane::new); - ResourceType physicsType = - registry.registerResource(CountingPhysicsWorldResource.class, - CountingPhysicsWorldResource::new); - PhysicsOwnerLifecycleSystem lifecycle = - new PhysicsOwnerLifecycleSystem(ownerType, physicsType); - registry.registerSystem(lifecycle); - - Store store = registry.addStore(testEntityStore("owner-lifecycle-test"), - EmptyResourceStorage.get()); - PhysicsOwnerResource owner = store.getResource(ownerType); - CountingPhysicsWorldResource physics = store.getResource(physicsType); - - assertInstanceOf(TestPhysicsOwnerLane.class, owner); - assertTrue(owner.isStarted()); - assertEquals(1, lifecycle.activeOwnerCount()); - assertTrue(physics.hasOwnerExecutor()); - - registry.removeStore(store); - - assertTrue(owner.isClosed()); - assertEquals(1, physics.clearCalls); - assertEquals(0, lifecycle.activeOwnerCount()); - assertTrue(physics.detachedOwnerPublication()); - lifecycle.close(); - registry.shutdown(); - } - - @Test - void storeAddAndRemoveAttachPooledOwnerLaneResource() { - ComponentRegistry registry = new ComponentRegistry<>(); - try (PhysicsOwnerLaneScheduler scheduler = new PhysicsOwnerLaneScheduler(1, - 4, - Duration.ofSeconds(2L))) { - ResourceType ownerType = - registry.registerResource(PhysicsOwnerResource.class, scheduler::createLane); - ResourceType physicsType = - registry.registerResource(CountingPhysicsWorldResource.class, - CountingPhysicsWorldResource::new); - PhysicsOwnerLifecycleSystem lifecycle = - new PhysicsOwnerLifecycleSystem(ownerType, physicsType); - registry.registerSystem(lifecycle); - - Store store = registry.addStore(testEntityStore("owner-lane-test"), - EmptyResourceStorage.get()); - PhysicsOwnerResource owner = store.getResource(ownerType); - CountingPhysicsWorldResource physics = store.getResource(physicsType); - - assertInstanceOf(PhysicsOwnerLaneResource.class, owner); - assertTrue(owner.isStarted()); - assertEquals(1, lifecycle.activeOwnerCount()); - assertTrue(physics.hasOwnerExecutor()); - - registry.removeStore(store); - - assertTrue(owner.isClosed()); - assertEquals(1, physics.clearCalls); - assertEquals(0, lifecycle.activeOwnerCount()); - assertTrue(physics.detachedOwnerPublication()); - lifecycle.close(); - } finally { - registry.shutdown(); - } - } - - @Test - void startFailureDoesNotAttachOwnerExecutor() { - ComponentRegistry registry = new ComponentRegistry<>(); - ResourceType ownerType = - registry.registerResource(FailingStartOwnerResource.class, - FailingStartOwnerResource::new); - ResourceType physicsType = - registry.registerResource(CountingPhysicsWorldResource.class, - CountingPhysicsWorldResource::new); - PhysicsOwnerLifecycleSystem lifecycle = - new PhysicsOwnerLifecycleSystem(ownerType, physicsType); - registry.registerSystem(lifecycle); - - Store store = registry.addStore(testEntityStore("start-failure-test"), - EmptyResourceStorage.get()); - FailingStartOwnerResource owner = store.getResource(ownerType); - CountingPhysicsWorldResource physics = store.getResource(physicsType); - - assertTrue(owner.startAttempted); - assertEquals(0, lifecycle.activeOwnerCount()); - assertTrue(physics.canAccessLiveBackendDirectly()); - - registry.removeStore(store); - lifecycle.close(); - registry.shutdown(); - } - - @Test - void closeClosesTrackedOwnersAsShutdownFallback() { - ComponentRegistry registry = new ComponentRegistry<>(); - ResourceType ownerLaneType = - registry.registerResource(TestPhysicsOwnerLane.class, - TestPhysicsOwnerLane::new); - ResourceType physicsType = - registry.registerResource(CountingPhysicsWorldResource.class, - CountingPhysicsWorldResource::new); - PhysicsOwnerLifecycleSystem lifecycle = - new PhysicsOwnerLifecycleSystem(ownerLaneType, physicsType); - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane()) { - - lifecycle.startOwner(owner, "fallback-world"); - assertTrue(owner.isStarted()); - assertEquals(1, lifecycle.activeOwnerCount()); - - lifecycle.close(); - - assertTrue(owner.isClosed()); - assertEquals(0, lifecycle.activeOwnerCount()); - } - registry.shutdown(); - } - - @Test - void removeStoreFallsBackToDirectClearWhenOwnerClearFails() { - ComponentRegistry registry = new ComponentRegistry<>(); - ResourceType ownerLaneType = - registry.registerResource(TestPhysicsOwnerLane.class, - TestPhysicsOwnerLane::new); - ResourceType physicsType = - registry.registerResource(FailingOncePhysicsWorldResource.class, - FailingOncePhysicsWorldResource::new); - PhysicsOwnerLifecycleSystem lifecycle = - new PhysicsOwnerLifecycleSystem(ownerLaneType, physicsType); - registry.registerSystem(lifecycle); - - Store store = registry.addStore(testEntityStore("fallback-test"), - EmptyResourceStorage.get()); - TestPhysicsOwnerLane owner = store.getResource(ownerLaneType); - FailingOncePhysicsWorldResource physics = store.getResource(physicsType); - - registry.removeStore(store); - - assertTrue(owner.isClosed()); - assertEquals(2, physics.clearCalls); - assertEquals(0, lifecycle.activeOwnerCount()); - lifecycle.close(); - registry.shutdown(); - } - - private static final class CountingPhysicsWorldResource extends LegacyLiveHandleTestResource { - - private int clearCalls; - private boolean detachedOwnerPublication; - - @Override - public void clearAllSpaces(@Nonnull String worldName) { - clearCalls++; - super.clearAllSpaces(worldName); - } - - private boolean hasOwnerExecutor() { - return !canAccessLiveBackendDirectly(); - } - - private boolean detachedOwnerPublication() { - return detachedOwnerPublication; - } - - @Override - public void detachOwnerExecutor(@Nonnull PhysicsOwnerHandle ownerExecutor) { - super.detachOwnerExecutor(ownerExecutor); - detachedOwnerPublication = canAccessLiveBackendDirectly(); - } - } - - private static final class FailingOncePhysicsWorldResource extends LegacyLiveHandleTestResource { - - private int clearCalls; - - @Override - public void clearAllSpaces(@Nonnull String worldName) { - clearCalls++; - if (clearCalls == 1) { - throw new IllegalStateException("forced clear failure"); - } - super.clearAllSpaces(worldName); - } - } - - private static final class FailingStartOwnerResource implements PhysicsOwnerResource { - - private boolean startAttempted; - private boolean closed; - - @Override - public void start(@Nonnull String worldName) { - startAttempted = true; - throw new IllegalStateException("forced start failure"); - } - - @Override - public boolean isStarted() { - return false; - } - - @Override - public boolean isClosed() { - return closed; - } - - @Override - public void close() { - closed = true; - } - - @Nonnull - @Override - public PhysicsOwnerResult submitAndDrain(@Nonnull PhysicsOwnerCommand command) - throws InterruptedException, ExecutionException { - throw new UnsupportedOperationException("not started"); - } - - @Override - public boolean submitStepIfIdle(@Nonnull PhysicsOwnerCommand command) { - throw new UnsupportedOperationException("not started"); - } - - @Nonnull - @Override - public PhysicsMutationHandle submitMutation(@Nonnull String operation, - @Nonnull PhysicsOwnerCommand command) { - throw new UnsupportedOperationException("not started"); - } - - @Nonnull - @Override - public PhysicsMutationHandle submitMutation(@Nonnull String operation, - @Nullable T value, - @Nonnull PhysicsOwnerCommand command) { - throw new UnsupportedOperationException("not started"); - } - - @Nonnull - @Override - public CompletableFuture submitMutationFuture( - @Nonnull String operation, - @Nonnull PhysicsOwnerCommand command) { - throw new UnsupportedOperationException("not started"); - } - - @Nonnull - @Override - public List pollCompletedMutations(int maxCompletions) { - return List.of(); - } - - @Nullable - @Override - public PhysicsOwnerStepCompletion pollCompletedStep() { - return null; - } - - @Override - public boolean hasPendingStep() { - return false; - } - - @Override - public long pendingStepAgeNanos() { - return 0L; - } - - @Override - public int pendingMutations() { - return 0; - } - - @Override - public int pendingCommands() { - return 0; - } - - @Override - public boolean isOwnerContext() { - return false; - } - - @Override - public void run(@Nonnull String operation, - @Nonnull PhysicsOwnerMutation mutation) { - throw new UnsupportedOperationException("not started"); - } - - @Nonnull - @Override - public PhysicsMutationHandle enqueue(@Nonnull String operation, - @Nullable T value, - @Nonnull PhysicsOwnerMutation mutation) { - throw new UnsupportedOperationException("not started"); - } - - @Nonnull - @Override - public CompletableFuture enqueueCall(@Nonnull String operation, - @Nonnull PhysicsOwnerCallable callable) { - throw new UnsupportedOperationException("not started"); - } - - @Nonnull - @Override - public T call(@Nonnull String operation, - @Nonnull PhysicsOwnerCallable callable) { - throw new UnsupportedOperationException("not started"); - } - - @Nonnull - @Override - public PhysicsOwnerResource clone() { - return new FailingStartOwnerResource(); - } - } - - @Nonnull - private static EntityStore testEntityStore(@Nonnull String worldName) { - return new EntityStore(TestInstanceFactory.world(worldName)); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsJointHydrationSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsJointHydrationSystemTest.java deleted file mode 100644 index c814eff7..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsJointHydrationSystemTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.persistence; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import org.junit.jupiter.api.Test; - -class PersistentPhysicsJointHydrationSystemTest { - - @Test - void hardJointHydrationFailureDoesNotFinalizeRestoreAsSuccessful() { - PersistentPhysicsWorldResource persistent = new PersistentPhysicsWorldResource(); - - persistent.markRuntimeRestorePending(); - persistent.failRuntimeRestore("forced joint failure"); - - assertFalse(PersistentPhysicsJointHydrationSystem.shouldFinalizeRuntimeRestore(persistent)); - } - - @Test - void pendingRestoreWithoutHardFailureCanFinalizeAfterJointHydration() { - PersistentPhysicsWorldResource persistent = new PersistentPhysicsWorldResource(); - - persistent.markRuntimeRestorePending(); - - assertTrue(PersistentPhysicsJointHydrationSystem.shouldFinalizeRuntimeRestore(persistent)); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsWorldSyncSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsWorldSyncSystemTest.java deleted file mode 100644 index 97157bec..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/persistence/PersistentPhysicsWorldSyncSystemTest.java +++ /dev/null @@ -1,571 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.persistence; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; -import dev.hytalemodding.impulse.api.PhysicsJoint; -import dev.hytalemodding.impulse.api.PhysicsJointType; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.runtime.BackendRuntimeCodes; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.FakePhysicsBackendRuntime; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsBodyState; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsJointState; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsRuntimeSnapshot; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsRuntimeSupport; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsSpaceState; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; -import dev.hytalemodding.impulse.core.internal.resources.BackendJointHandle; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistration; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.simulation.JointType; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsEventCollectionMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepSchedulingMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; -import javax.annotation.Nonnull; -import org.joml.Quaternionf; -import org.joml.Vector3d; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class PersistentPhysicsWorldSyncSystemTest { - - private static final AtomicInteger BACKEND_COUNTER = new AtomicInteger(); - - @Test - void persistentBodyCountChangeForcesSnapshotBeforeCadence() { - RuntimeFixture fixture = createRuntimeFixture(); - fixture.syncPersistentSpaces(); - fixture.persistent.markRuntimeSnapshotSynced(); - - assertFalse(fixture.persistent.shouldSyncRuntimeSnapshot(20)); - assertFalse(PersistentPhysicsWorldSyncSystem.hasRuntimePersistenceFootprintChanged( - fixture.persistent, - PersistentPhysicsRuntimeSnapshot.captureFootprint(fixture.runtime))); - - PhysicsBody body = fixture.space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - fixture.runtime.addBody(fixture.space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - - assertTrue(PersistentPhysicsWorldSyncSystem.hasRuntimePersistenceFootprintChanged( - fixture.persistent, - PersistentPhysicsRuntimeSnapshot.captureFootprint(fixture.runtime))); - } - - @Test - void runtimeOnlyBodiesDoNotChangePersistentFootprint() { - RuntimeFixture fixture = createRuntimeFixture(); - fixture.syncPersistentSpaces(); - fixture.persistent.markRuntimeSnapshotSynced(); - - PhysicsBody body = fixture.space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - fixture.runtime.addBody(fixture.space.id(), - body, - PhysicsBodyKind.TEMPORARY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - - assertFalse(PersistentPhysicsWorldSyncSystem.hasRuntimePersistenceFootprintChanged( - fixture.persistent, - PersistentPhysicsRuntimeSnapshot.captureFootprint(fixture.runtime))); - } - - @Test - void persistentJointCountChangeForcesSnapshotBeforeCadence() { - RuntimeFixture fixture = createRuntimeFixture(); - PhysicsBody first = fixture.space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody second = fixture.space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - RigidBodyKey firstId = fixture.runtime.addBody(fixture.space.id(), - first, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - RigidBodyKey secondId = fixture.runtime.addBody(fixture.space.id(), - second, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - fixture.syncPersistentSpaces(); - fixture.persistent.setBodies(new PersistentPhysicsBodyState[] { - fixture.bodyState(firstId), - fixture.bodyState(secondId) - }); - fixture.persistent.markRuntimeSnapshotSynced(); - - assertFalse(fixture.persistent.shouldSyncRuntimeSnapshot(20)); - assertFalse(PersistentPhysicsWorldSyncSystem.hasRuntimePersistenceFootprintChanged( - fixture.persistent, - PersistentPhysicsRuntimeSnapshot.captureFootprint(fixture.runtime))); - - PhysicsJoint joint = fixture.space.createFixedJoint(first, second, new Vector3f(), new Vector3f()); - fixture.runtime.addJoint(fixture.space.id(), joint); - - assertTrue(PersistentPhysicsWorldSyncSystem.hasRuntimePersistenceFootprintChanged( - fixture.persistent, - PersistentPhysicsRuntimeSnapshot.captureFootprint(fixture.runtime))); - } - - @Test - void explicitRuntimeSnapshotSyncCopiesPersistentState() { - RuntimeFixture fixture = createRuntimeFixture(); - PhysicsWorldSettings settings = fixture.runtime.getWorldSettings(); - settings.setStepSchedulingMode(PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT); - settings.setEventCollectionMode(PhysicsEventCollectionMode.CONTACTS); - fixture.runtime.setWorldSettings(settings); - PhysicsBody body = fixture.space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - fixture.runtime.addBody(fixture.space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - - PersistentPhysicsWorldSyncSystem.SyncResult result = - PersistentPhysicsWorldSyncSystem.syncRuntimeSnapshot(fixture.persistent, - PersistentPhysicsRuntimeSnapshot.capture(fixture.runtime)); - - assertTrue(result.synced()); - assertEquals(1, result.spaces()); - assertEquals(1, result.bodies()); - assertEquals(0, result.joints()); - assertEquals(1, fixture.persistent.getSpaceCount()); - assertEquals(1, fixture.persistent.getBodyCount()); - assertEquals(0, fixture.persistent.getJointCount()); - assertEquals(PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT, - fixture.persistent.getWorldSettings().getStepSchedulingMode()); - assertEquals(PhysicsEventCollectionMode.CONTACTS, - fixture.persistent.getWorldSettings().getEventCollectionMode()); - } - - @Test - void runtimeSnapshotCaptureUsesLiveBodyPoseWhenReaderSnapshotIsStale() { - RuntimeFixture fixture = createRuntimeFixture(); - PhysicsBody body = fixture.space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - body.setPosition(1.0f, 2.0f, 3.0f); - RigidBodyKey bodyKey = fixture.runtime.addBody(fixture.space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - fixture.runtime.refreshBodySnapshots(); - - body.setPosition(9.0f, 8.0f, 7.0f); - - PhysicsBodySnapshot readerSnapshot = fixture.runtime.getBodySnapshot(bodyKey); - assertEquals(1.0f, readerSnapshot.positionX(), 0.0001f); - assertEquals(2.0f, readerSnapshot.positionY(), 0.0001f); - assertEquals(3.0f, readerSnapshot.positionZ(), 0.0001f); - - PersistentPhysicsRuntimeSnapshot snapshot = - PersistentPhysicsRuntimeSnapshot.capture(fixture.runtime); - PersistentPhysicsBodyState state = snapshot.getBodies()[0]; - - assertEquals(9.0f, state.getPosition().x, 0.0001f); - assertEquals(8.0f, state.getPosition().y, 0.0001f); - assertEquals(7.0f, state.getPosition().z, 0.0001f); - } - - @Test - void explicitRuntimeSnapshotSyncSkipsPendingRestore() { - RuntimeFixture fixture = createRuntimeFixture(); - fixture.persistent.markRuntimeRestorePending(); - - PersistentPhysicsWorldSyncSystem.SyncResult result = - PersistentPhysicsWorldSyncSystem.syncRuntimeSnapshot(fixture.persistent, - PersistentPhysicsRuntimeSnapshot.capture(fixture.runtime)); - - assertFalse(result.synced()); - assertEquals("restore pending", result.skippedReason()); - } - - @Test - void explicitRuntimeSnapshotSyncSkipsFailedRestore() { - RuntimeFixture fixture = createRuntimeFixture(); - fixture.persistent.failRuntimeRestore("bad persisted state"); - - PersistentPhysicsWorldSyncSystem.SyncResult result = - PersistentPhysicsWorldSyncSystem.syncRuntimeSnapshot(fixture.persistent, - PersistentPhysicsRuntimeSnapshot.capture(fixture.runtime)); - - assertFalse(result.synced()); - assertEquals("restore failed", result.skippedReason()); - } - - @Test - void softRestoreSkipsDoNotBlockRuntimeSnapshotTickPolicy() { - RuntimeFixture fixture = createRuntimeFixture(); - - fixture.persistent.recordRuntimeBodySkipped("missing entity"); - - assertFalse(PersistentPhysicsWorldSyncSystem.shouldSkipRuntimeSnapshotTick(fixture.persistent)); - - fixture.persistent.markRuntimeRestorePending(); - - assertTrue(PersistentPhysicsWorldSyncSystem.shouldSkipRuntimeSnapshotTick(fixture.persistent)); - } - - @Test - void runtimeSnapshotSyncSkipsChangedRestoreGeneration() { - RuntimeFixture fixture = createRuntimeFixture(); - PhysicsBody body = fixture.space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - fixture.runtime.addBody(fixture.space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - PersistentPhysicsRuntimeSnapshot snapshot = - PersistentPhysicsRuntimeSnapshot.capture(fixture.runtime); - long capturedGeneration = fixture.persistent.runtimeRestoreGeneration(); - - fixture.persistent.markRuntimeRestorePending(); - fixture.persistent.clearRuntimeRestorePending(); - - PersistentPhysicsWorldSyncSystem.SyncResult result = - PersistentPhysicsWorldSyncSystem.syncRuntimeSnapshot(fixture.persistent, - snapshot, - capturedGeneration); - - assertFalse(result.synced()); - assertEquals("restore generation changed", result.skippedReason()); - assertEquals(0, fixture.persistent.getBodyCount()); - } - - @Test - void bodyHydrationChecksLiveRegistrationBeforeCreatingBackendBody() { - RuntimeFixture fixture = createRuntimeFixture(); - PhysicsBody body = fixture.space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - RigidBodyKey bodyKey = fixture.runtime.addBody(fixture.space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - PersistentPhysicsBodyState state = fixture.bodyState(bodyKey); - - PersistentPhysicsBodyHydrationSystem.RestoreBodyResult result = - PersistentPhysicsBodyHydrationSystem.restoreBodyOnOwner(fixture.runtime, state, bodyKey); - - assertEquals(PersistentPhysicsBodyHydrationSystem.RestoreBodyResult.ALREADY_REGISTERED, result); - assertEquals(1, fixture.space.bodyCount()); - } - - @Test - void bodyHydrationRemovesBackendBodyWhenRegistrationFails() { - BackendRuntimeFixture fixture = createBackendRuntimeFixture("body-cleanup"); - FailingRegistrationResource runtime = fixture.runtime(); - RigidBodyKey bodyKey = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000101")); - PersistentPhysicsBodyState state = persistentBodyState(fixture.spaceId(), bodyKey); - runtime.failBodyRegistration = true; - - assertThrows(IllegalStateException.class, - () -> PersistentPhysicsBodyHydrationSystem.restoreBodyOnOwner(runtime, state, bodyKey)); - - assertEquals(0, fixture.backendRuntime().bodyCount(fixture.spaceId().value())); - } - - @Test - void jointHydrationRemovesBackendJointWhenRegistrationFails() { - BackendRuntimeFixture fixture = createBackendRuntimeFixture("joint-cleanup"); - FailingRegistrationResource runtime = fixture.runtime(); - RigidBodyKey bodyAKey = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000102")); - RigidBodyKey bodyBKey = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000103")); - PhysicsSpaceBinding binding = runtime.requireSpaceBinding(fixture.spaceId()); - BackendBodyHandle bodyAHandle = createBackendBox(binding); - BackendBodyHandle bodyBHandle = createBackendBox(binding); - runtime.addBodyOnOwner(bodyAKey, - fixture.spaceId(), - bodyAHandle, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - runtime.addBodyOnOwner(bodyBKey, - fixture.spaceId(), - bodyBHandle, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - PersistentPhysicsJointState state = new PersistentPhysicsJointState(); - state.setSpaceId(fixture.spaceId().value()); - state.setBodyAKey(bodyAKey); - state.setBodyBKey(bodyBKey); - state.setType(PhysicsJointType.FIXED); - runtime.failJointRegistration = true; - - assertThrows(IllegalStateException.class, - () -> PersistentPhysicsRuntimeSupport.createJoint(runtime, - binding, - state, - bodyAKey, - runtime.requireBodyRegistration(bodyAKey), - bodyBKey, - runtime.requireBodyRegistration(bodyBKey))); - - assertEquals(0, fixture.backendRuntime().jointCount(fixture.spaceId().value())); - } - - @Test - void persistentBodyStateCapturesRuntimeMaterialAndCollisionSettings() { - RuntimeFixture fixture = createRuntimeFixture(); - PhysicsBody body = fixture.space.createBox(1.0f, 2.0f, 3.0f, 7.0f); - body.setFriction(0.65f); - body.setRestitution(0.15f); - body.setDamping(0.2f, 0.3f); - body.setCollisionFilter(PhysicsCollisionFilters.DYNAMIC_BODY, - PhysicsCollisionFilters.TERRAIN | PhysicsCollisionFilters.DYNAMIC_BODY); - body.setContinuousCollisionEnabled(true); - RigidBodyKey bodyKey = fixture.runtime.addBody(fixture.space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - - PersistentPhysicsBodyState state = fixture.bodyState(bodyKey); - - assertEquals(7.0f, state.getMass(), 0.0001f); - assertEquals(0.65f, state.getFriction(), 0.0001f); - assertEquals(0.15f, state.getRestitution(), 0.0001f); - assertEquals(0.2f, state.getLinearDamping(), 0.0001f); - assertEquals(0.3f, state.getAngularDamping(), 0.0001f); - assertEquals(PhysicsCollisionFilters.DYNAMIC_BODY, state.getCollisionGroup()); - assertEquals(PhysicsCollisionFilters.TERRAIN | PhysicsCollisionFilters.DYNAMIC_BODY, - state.getCollisionMask()); - assertTrue(state.isContinuousCollisionEnabled()); - } - - @Test - void restoreTerrainPrewarmTargetsOnlyRestoredDynamicBodies() { - RuntimeFixture fixture = createRuntimeFixture(); - PhysicsBody fallingBody = fixture.space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - fallingBody.setPosition(12.0f, 70.0f, -4.0f); - PhysicsBody staticBody = fixture.space.createBox(0.5f, 0.5f, 0.5f, 0.0f); - staticBody.setBodyType(PhysicsBodyType.STATIC); - staticBody.setPosition(40.0f, 64.0f, 40.0f); - RigidBodyKey fallingBodyKey = fixture.runtime.addBody(fixture.space.id(), - fallingBody, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - RigidBodyKey staticBodyKey = fixture.runtime.addBody(fixture.space.id(), - staticBody, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - PersistentPhysicsBodyState fallingState = fixture.bodyState(fallingBodyKey); - PersistentPhysicsBodyState staticState = fixture.bodyState(staticBodyKey); - assertEquals(PhysicsBodyType.DYNAMIC, fallingState.getBodyType()); - assertEquals(PhysicsBodyType.STATIC, staticState.getBodyType()); - - Map> targets = - PersistentPhysicsRestoreTerrainPrewarm.dynamicPrewarmTargetsBySpace( - new PersistentPhysicsBodyState[] { - fallingState, - staticState - }, - 4); - - assertEquals(List.of(new Vector3d(12.0, 70.0, -4.0)), - targets.get(fixture.space.id().value())); - } - - @Test - void restoreTerrainPrewarmExpandsDownwardForFallingBodies() { - RuntimeFixture fixture = createRuntimeFixture(); - PhysicsBody fallingBody = fixture.space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - fallingBody.setPosition(12.0f, 20.0f, -4.0f); - fallingBody.setLinearVelocity(0.0f, -12.0f, 0.0f); - RigidBodyKey fallingBodyKey = fixture.runtime.addBody(fixture.space.id(), - fallingBody, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - PersistentPhysicsBodyState fallingState = fixture.bodyState(fallingBodyKey); - - Map> targets = - PersistentPhysicsRestoreTerrainPrewarm.dynamicPrewarmTargetsBySpace( - new PersistentPhysicsBodyState[] {fallingState}, - 4); - - assertEquals(List.of(new Vector3d(12.0, 20.0, -4.0), - new Vector3d(12.0, 12.0, -4.0), - new Vector3d(12.0, 4.0, -4.0)), - targets.get(fixture.space.id().value())); - } - - private static RuntimeFixture createRuntimeFixture() { - FakePhysicsBackend backend = - new FakePhysicsBackend("test:persistence-sync-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - - LegacyLiveHandleTestResource runtime = new LegacyLiveHandleTestResource(); - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - PhysicsSpace space = runtime.createLiveSpace(backend.getId(), - "test-world", - settings); - PersistentPhysicsWorldResource persistent = new PersistentPhysicsWorldResource(); - return new RuntimeFixture(runtime, persistent, space); - } - - private static BackendRuntimeFixture createBackendRuntimeFixture(@Nonnull String name) { - BackendId backendId = - new BackendId("test:persistence-" + name + "-" + BACKEND_COUNTER.incrementAndGet()); - FakePhysicsBackendRuntimeProvider provider = - new FakePhysicsBackendRuntimeProvider(backendId, false, false); - Impulse.registerRuntimeProvider(provider); - FailingRegistrationResource runtime = new FailingRegistrationResource(); - SpaceId spaceId = runtime.createSpace(backendId, - "test-world", - PhysicsSpaceSettings.defaults()); - return new BackendRuntimeFixture(runtime, provider.createdRuntimes().getFirst(), spaceId); - } - - @Nonnull - private static PersistentPhysicsBodyState persistentBodyState(@Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey bodyKey) { - PhysicsBodyRegistration registration = new PhysicsBodyRegistration(bodyKey, - new BackendBodyHandle(1L), - spaceId, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT); - return PersistentPhysicsBodyState.from(registration, - new PhysicsBodySnapshot(new Vector3f(), - new Quaternionf(), - new Vector3f(), - new Vector3f(), - PhysicsBodyType.DYNAMIC, - false, - false, - 1.0f, - ShapeType.BOX, - new Vector3f(0.5f, 0.5f, 0.5f), - 0.0f, - 0.0f, - PhysicsAxis.Y)); - } - - @Nonnull - private static BackendBodyHandle createBackendBox(@Nonnull PhysicsSpaceBinding binding) { - return new BackendBodyHandle(binding.runtime().createBody(binding.backendSpaceHandle().value(), - BackendRuntimeCodes.SHAPE_BOX, - 0.5f, - 0.5f, - 0.5f, - 0.0f, - 0.0f, - BackendRuntimeCodes.AXIS_Y, - 0.0f, - 1.0f, - BackendRuntimeCodes.BODY_DYNAMIC, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 1.0f)); - } - - private record RuntimeFixture(LegacyLiveHandleTestResource runtime, - PersistentPhysicsWorldResource persistent, - PhysicsSpace space) { - - private void syncPersistentSpaces() { - persistent.setSpaces(new PersistentPhysicsSpaceState[] { - PersistentPhysicsSpaceState.from(runtime.requireSpaceBinding(space.id()), - runtime.getSpaceSettings(space.id())) - }); - } - - private PersistentPhysicsBodyState bodyState(RigidBodyKey bodyKey) { - return PersistentPhysicsBodyState.from(runtime.requireBodyRegistration(bodyKey), - runtime.getBodySnapshot(bodyKey)); - } - } - - private record BackendRuntimeFixture(@Nonnull FailingRegistrationResource runtime, - @Nonnull FakePhysicsBackendRuntime backendRuntime, - @Nonnull SpaceId spaceId) { - } - - private static final class FailingRegistrationResource extends PhysicsWorldRuntimeResource { - - private boolean failBodyRegistration; - private boolean failJointRegistration; - - @Nonnull - @Override - public RigidBodyKey addBodyOnOwner(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull BackendBodyHandle backendBodyHandle, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - if (failBodyRegistration) { - throw new IllegalStateException("forced body registration failure"); - } - return super.addBodyOnOwner(bodyKey, spaceId, backendBodyHandle, kind, persistenceMode); - } - - @Nonnull - @Override - public JointKey addJointOnOwner(@Nonnull JointKey jointKey, - @Nonnull SpaceId spaceId, - @Nonnull BackendJointHandle backendJointHandle, - @Nonnull RigidBodyKey bodyA, - @Nonnull RigidBodyKey bodyB, - @Nonnull JointType type, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ, - float restLength, - float stiffness, - float damping, - float lowerLimit, - float upperLimit, - boolean motorEnabled, - float motorTargetVelocity, - float motorMaxForce) { - if (failJointRegistration) { - throw new IllegalStateException("forced joint registration failure"); - } - return super.addJointOnOwner(jointKey, - spaceId, - backendJointHandle, - bodyA, - bodyB, - type, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - axisX, - axisY, - axisZ, - restLength, - stiffness, - damping, - lowerLimit, - upperLimit, - motorEnabled, - motorTargetVelocity, - motorMaxForce); - } - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/publication/PhysicsPublicationPipelineTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/publication/PhysicsPublicationPipelineTest.java deleted file mode 100644 index 789dee39..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/publication/PhysicsPublicationPipelineTest.java +++ /dev/null @@ -1,132 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.publication; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.TestPhysicsOwnerLane; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerSnapshot; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerStepCommand; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import org.junit.jupiter.api.Test; - -class PhysicsPublicationPipelineTest { - - @Test - void completedMutationsDrainThroughPublicationPipelineCap() throws Exception { - AtomicInteger mutations = new AtomicInteger(); - int mutationCount = 80; - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane()) { - owner.start("publication-pipeline-mutation-drain-test"); - for (int index = 0; index < mutationCount; index++) { - owner.submitMutation("test mutation " + index, () -> { - mutations.incrementAndGet(); - return PhysicsOwnerSnapshot.empty(); - }); - } - - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2L); - while (System.nanoTime() < deadline && owner.pendingCommands() > 0) { - Thread.sleep(10L); - } - - assertEquals(64, PhysicsPublicationPipeline.publishCompletedMutations(owner)); - assertEquals(16, PhysicsPublicationPipeline.publishCompletedMutations(owner)); - assertEquals(mutationCount, mutations.get()); - } - } - - @Test - void completedStepRecordsPreStepDrainBackpressure() throws Exception { - CountDownLatch activeMutationStarted = new CountDownLatch(1); - CountDownLatch releaseActiveMutation = new CountDownLatch(1); - CountDownLatch preCutoffMutationStarted = new CountDownLatch(1); - CountDownLatch releasePreCutoffMutation = new CountDownLatch(1); - CountDownLatch releasePostCutoffMutation = new CountDownLatch(1); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsRuntimeProfilingResource profiling = new PhysicsRuntimeProfilingResource(); - profiling.setEnabled(true); - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane()) { - owner.start("publication-pipeline-pre-step-drain-test"); - owner.submitMutation("active mutation", () -> { - activeMutationStarted.countDown(); - assertTrue(releaseActiveMutation.await(2L, TimeUnit.SECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - assertTrue(activeMutationStarted.await(2L, TimeUnit.SECONDS)); - - owner.submitMutation("pre-cutoff mutation", () -> { - preCutoffMutationStarted.countDown(); - assertTrue(releasePreCutoffMutation.await(2L, TimeUnit.SECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - - assertTrue(owner.submitStepIfIdle(new PhysicsOwnerStepCommand(resource, - 0.05f, - false, - 1L, - 1L))); - - owner.submitMutation("post-cutoff mutation", () -> { - assertTrue(releasePostCutoffMutation.await(2L, TimeUnit.SECONDS)); - return PhysicsOwnerSnapshot.empty(); - }); - - releaseActiveMutation.countDown(); - assertTrue(preCutoffMutationStarted.await(2L, TimeUnit.SECONDS)); - releasePreCutoffMutation.countDown(); - - PhysicsEventFrame eventFrame = publishStepWhenReady(owner, resource, profiling); - - assertNotNull(eventFrame); - assertEquals(1, profiling.getCumulativeStep().getTickSamples()); - assertEquals(1, profiling.getCumulativeStep().getPreStepDrainedMutations()); - assertTrue(profiling.getCumulativeStep().getPreStepDrainRunNanos() > 0L); - assertEquals(1, profiling.getCumulativeStep().getLateMutationBacklogAtStep()); - assertEquals(1, profiling.getCumulativeStep().getMaxLateMutationBacklogAtStep()); - - releasePostCutoffMutation.countDown(); - assertEquals(3, waitForCompletedMutations(owner, 3)); - } finally { - releaseActiveMutation.countDown(); - releasePreCutoffMutation.countDown(); - releasePostCutoffMutation.countDown(); - } - } - - private static PhysicsEventFrame publishStepWhenReady(TestPhysicsOwnerLane owner, - LegacyLiveHandleTestResource resource, - PhysicsRuntimeProfilingResource profiling) throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2L); - PhysicsEventFrame eventFrame = null; - while (System.nanoTime() < deadline && eventFrame == null) { - eventFrame = PhysicsPublicationPipeline.publishCompletedStep(owner, - resource, - profiling, - 1L); - if (eventFrame == null) { - Thread.sleep(10L); - } - } - return eventFrame; - } - - private static int waitForCompletedMutations(TestPhysicsOwnerLane owner, - int expected) throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2L); - int completions = 0; - while (System.nanoTime() < deadline && completions < expected) { - completions += PhysicsPublicationPipeline.publishCompletedMutations(owner); - if (completions < expected) { - Thread.sleep(10L); - } - } - return completions; - } - -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/publication/PhysicsSnapshotPublicationSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/publication/PhysicsSnapshotPublicationSystemTest.java deleted file mode 100644 index c154e96a..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/publication/PhysicsSnapshotPublicationSystemTest.java +++ /dev/null @@ -1,355 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.publication; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.hypixel.hytale.component.SystemGroup; -import com.hypixel.hytale.component.dependency.Dependency; -import com.hypixel.hytale.component.dependency.Order; -import com.hypixel.hytale.component.dependency.SystemGroupDependency; -import com.hypixel.hytale.component.dependency.SystemDependency; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsContact; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.TestPhysicsOwnerLane; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerResource; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems.PhysicsChunkBoundarySystem; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems.PhysicsCollisionLodSystem; -import dev.hytalemodding.impulse.core.internal.modules.worldcollision.systems.PhysicsWorldCollisionStreamingSystem; -import dev.hytalemodding.impulse.core.internal.systems.persistence.PersistentPhysicsWorldSyncSystem; -import dev.hytalemodding.impulse.core.internal.systems.sync.PhysicsSyncSystem; -import dev.hytalemodding.impulse.core.internal.systems.visual.PhysicsDetachedVisualMaterializationSystem; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerBridge; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerSnapshot; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerStepCommand; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsEventCollectionMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.util.Set; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class PhysicsSnapshotPublicationSystemTest { - - @Test - void publicationDependenciesKeepReadersBehindSnapshotApply() throws Exception { - Set> dependencies = - new PhysicsSnapshotPublicationSystem().getDependencies(); - - assertNoSystemDependency(dependencies, PhysicsCollisionLodSystem.class); - assertNoSystemDependency(dependencies, PhysicsChunkBoundarySystem.class); - assertNoSystemDependency(dependencies, PhysicsWorldCollisionStreamingSystem.class); - assertSystemDependency(dependencies, - Order.BEFORE, - PhysicsDetachedVisualMaterializationSystem.class); - assertSystemDependency(dependencies, Order.BEFORE, PhysicsSyncSystem.class); - assertSystemDependency(new PhysicsCollisionLodSystem().getDependencies(), - Order.AFTER, - PhysicsSnapshotPublicationSystem.class); - assertSystemDependency(new PhysicsWorldCollisionStreamingSystem().getDependencies(), - Order.AFTER, - PhysicsSnapshotPublicationSystem.class); - assertSystemDependency(new PersistentPhysicsWorldSyncSystem().getDependencies(), - Order.AFTER, - PhysicsSnapshotPublicationSystem.class); - withTestImpulsePlugin(() -> { - assertSystemDependency(new PhysicsChunkBoundarySystem().getDependencies(), - Order.AFTER, - PhysicsSnapshotPublicationSystem.class); - assertSystemGroupDependency( - new PhysicsDetachedVisualMaterializationSystem().getDependencies(), - Order.AFTER, - ImpulsePlugin.get().getPersistenceRestoreGroup()); - }); - } - - @Test - void completedOwnerStepPublishesSnapshotWithoutDrain() throws Exception { - BackendId backendId = new BackendId("test:async-publication"); - Impulse.registerBackend(new FakePhysicsBackend(backendId)); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - var settings = resource.getWorldSettings(); - settings.setStepMode(PhysicsStepMode.FIXED); - settings.setSimulationSteps(1); - settings.setEventCollectionMode(PhysicsEventCollectionMode.CONTACTS); - resource.setWorldSettings(settings); - PhysicsRuntimeProfilingResource profiling = new PhysicsRuntimeProfilingResource(); - profiling.setEnabled(true); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane()) { - owner.start("async-publication-test"); - resource.attachOwnerExecutor(owner); - PhysicsSpace space = resource.createLiveSpace(backendId, - "async-publication-test", - PhysicsSpaceSettings.defaults()); - FakePhysicsBackend.InMemoryPhysicsSpace inMemorySpace = - (FakePhysicsBackend.InMemoryPhysicsSpace) space; - AtomicReference bodyRef = new AtomicReference<>(); - RigidBodyKey bodyId = PhysicsOwnerBridge.call(owner, - "create async publication body", - () -> { - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - body.setPosition(1.0f, 2.0f, 3.0f); - bodyRef.set(body); - return resource.addBody(space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - }); - PhysicsBody secondBody = PhysicsOwnerBridge.call(owner, - "create async publication contact body", - () -> { - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - resource.addBody(space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - return body; - }); - inMemorySpace.addContact(new PhysicsContact(bodyRef.get(), - secondBody, - new Vector3f(1.0f, 2.0f, 3.0f), - new Vector3f(4.0f, 5.0f, 6.0f), - new Vector3f(0.0f, 1.0f, 0.0f), - -0.125f, - 2.5f)); - assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), - resource.getBodySnapshot(bodyId).position()); - - PhysicsOwnerBridge.run(owner, "move async publication body", - () -> bodyRef.get().setPosition(4.0f, 5.0f, 6.0f)); - assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), - resource.getBodySnapshot(bodyId).position()); - - PhysicsOwnerStepCommand command = new PhysicsOwnerStepCommand(resource, - 0.05f, - true, - 1L, - 1L); - assertTrue(owner.submitStepIfIdle(command)); - assertFalse(owner.submitStepIfIdle(new PhysicsOwnerStepCommand(resource, - 0.05f, - false, - 2L, - 2L))); - - PhysicsEventFrame eventFrame = publishWhenReady(owner, resource, profiling); - - PhysicsBodySnapshot snapshot = resource.getBodySnapshot(bodyId); - assertEquals(new Vector3f(4.0f, 5.0f, 6.0f), snapshot.position()); - assertEquals(1, eventFrame.physicsEventCount()); - assertEquals(1, profiling.getCumulativeStep().getTickSamples()); - assertEquals(2, profiling.getCumulativeStep().getBodySnapshots()); - assertTrue(profiling.getCumulativeStep().getOwnerRunNanos() > 0L); - assertTrue(profiling.getCumulativeStep().getOwnerQueuedNanos() >= 0L); - assertFalse(owner.hasPendingStep()); - } - } - - @Test - void publicationSystemDrainsCompletedAsyncMutations() throws Exception { - AtomicInteger mutations = new AtomicInteger(); - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane()) { - owner.start("async-mutation-publication-test"); - owner.submitMutation("test mutation", () -> { - mutations.incrementAndGet(); - return PhysicsOwnerSnapshot.empty(); - }); - - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2L); - int published = 0; - while (System.nanoTime() < deadline && published == 0) { - published += PhysicsSnapshotPublicationSystem.publishCompletedMutations(owner); - if (published == 0) { - Thread.sleep(10L); - } - } - - assertEquals(1, published); - assertEquals(1, mutations.get()); - assertEquals(0, owner.pendingMutations()); - } - } - - @Test - void publicationSystemCapsCompletedAsyncMutationDrainPerTick() throws Exception { - AtomicInteger mutations = new AtomicInteger(); - int mutationCount = 80; - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane()) { - owner.start("async-mutation-publication-cap-test"); - for (int index = 0; index < mutationCount; index++) { - owner.submitMutation("test mutation " + index, () -> { - mutations.incrementAndGet(); - return PhysicsOwnerSnapshot.empty(); - }); - } - - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2L); - while (System.nanoTime() < deadline && owner.pendingCommands() > 0) { - Thread.sleep(10L); - } - assertEquals(0, owner.pendingCommands()); - assertEquals(mutationCount, mutations.get()); - - int firstTickPublished = PhysicsSnapshotPublicationSystem.publishCompletedMutations(owner); - - assertEquals(64, firstTickPublished); - assertEquals(mutationCount - 64, owner.pendingMutations()); - assertEquals(mutationCount - 64, - PhysicsSnapshotPublicationSystem.publishCompletedMutations(owner)); - assertEquals(0, owner.pendingMutations()); - } - } - - @Test - void completedOwnerStepDoesNotRepublishAfterWorldMutation() throws Exception { - BackendId backendId = new BackendId("test:stale-owner-publication"); - Impulse.registerBackend(new FakePhysicsBackend(backendId)); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsRuntimeProfilingResource profiling = new PhysicsRuntimeProfilingResource(); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane()) { - owner.start("stale-owner-publication-test"); - resource.attachOwnerExecutor(owner); - PhysicsSpace space = resource.createLiveSpace(backendId, - "stale-owner-publication-test", - PhysicsSpaceSettings.defaults()); - RigidBodyKey bodyId = PhysicsOwnerBridge.call(owner, - "create stale publication body", - () -> { - PhysicsBody body = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - body.setPosition(1.0f, 2.0f, 3.0f); - return resource.addBody(space.id(), - body, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - }); - - PhysicsOwnerStepCommand command = new PhysicsOwnerStepCommand(resource, - 0.05f, - false, - 1L, - 1L); - assertTrue(owner.submitStepIfIdle(command)); - waitForPublishedFrame(command); - assertEquals(1, command.publishedFrame().bodyCount()); - - resource.destroyBody(bodyId); - assertEquals(0, resource.getBodySnapshotCount()); - assertNull(resource.getBodyRegistrationView(bodyId)); - - PhysicsEventFrame eventFrame = - PhysicsSnapshotPublicationSystem.publishCompletedStep(owner, resource, profiling); - - assertNull(eventFrame); - assertFalse(owner.hasPendingStep()); - assertEquals(0, resource.getBodySnapshotCount()); - assertNull(resource.getBodyRegistrationView(bodyId)); - resource.detachOwnerExecutor(owner); - } - } - - private static PhysicsEventFrame publishWhenReady(PhysicsOwnerResource owner, - LegacyLiveHandleTestResource resource, - PhysicsRuntimeProfilingResource profiling) throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2L); - PhysicsEventFrame eventFrame = null; - while (System.nanoTime() < deadline) { - eventFrame = PhysicsSnapshotPublicationSystem.publishCompletedStep(owner, resource, profiling); - if (!owner.hasPendingStep()) { - return eventFrame; - } - Thread.sleep(10L); - } - eventFrame = PhysicsSnapshotPublicationSystem.publishCompletedStep(owner, resource, profiling); - assertFalse(owner.hasPendingStep()); - return eventFrame; - } - - private static void waitForPublishedFrame(PhysicsOwnerStepCommand command) - throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2L); - while (System.nanoTime() < deadline && command.publishedFrame() == null) { - Thread.sleep(10L); - } - assertTrue(command.publishedFrame() != null); - } - - private static void assertSystemDependency(Set> dependencies, - Order order, - Class systemClass) { - assertTrue(dependencies.stream().anyMatch(dependency -> - dependency.getOrder() == order - && dependency instanceof SystemDependency systemDependency - && systemDependency.getSystemClass().equals(systemClass))); - } - - private static void assertNoSystemDependency(Set> dependencies, - Class systemClass) { - assertFalse(dependencies.stream().anyMatch(dependency -> - dependency instanceof SystemDependency systemDependency - && systemDependency.getSystemClass().equals(systemClass))); - } - - private static void assertSystemGroupDependency(Set> dependencies, - Order order, - Object group) { - assertTrue(dependencies.stream().anyMatch(dependency -> - dependency.getOrder() == order - && dependency instanceof SystemGroupDependency groupDependency - && groupDependency.getGroup().equals(group))); - } - - private static void withTestImpulsePlugin(ThrowingRunnable assertion) throws Exception { - SystemGroup restoreGroup = testSystemGroup(); - ImpulsePlugin plugin = TestInstanceFactory.impulsePlugin(); - Field groupField = ImpulsePlugin.class.getDeclaredField("persistenceRestoreGroup"); - groupField.setAccessible(true); - groupField.set(plugin, restoreGroup); - - Field instanceField = ImpulsePlugin.class.getDeclaredField("instance"); - instanceField.setAccessible(true); - Object previous = instanceField.get(null); - instanceField.set(null, plugin); - try { - assertion.run(); - } finally { - instanceField.set(null, previous); - } - } - - @SuppressWarnings("unchecked") - private static SystemGroup testSystemGroup() throws Exception { - Class componentRegistry = Class.forName("com.hypixel.hytale.component.ComponentRegistry"); - Constructor constructor = - SystemGroup.class.getDeclaredConstructor(componentRegistry, int.class, Set.class); - constructor.setAccessible(true); - return (SystemGroup) constructor.newInstance(null, 1, Set.of()); - } - - @FunctionalInterface - private interface ThrowingRunnable { - - void run() throws Exception; - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepRestoreGateTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepRestoreGateTest.java deleted file mode 100644 index 209e3781..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepRestoreGateTest.java +++ /dev/null @@ -1,29 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.step; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import org.junit.jupiter.api.Test; - -class PhysicsStepRestoreGateTest { - - @Test - void allowsStepSubmissionWhenRestoreIsSettled() { - PersistentPhysicsWorldResource persistent = new PersistentPhysicsWorldResource(); - - assertTrue(PhysicsStepRestoreGate.canSubmitStep(persistent)); - } - - @Test - void blocksStepSubmissionDuringPendingOrFailedRestore() { - PersistentPhysicsWorldResource pending = new PersistentPhysicsWorldResource(); - pending.markRuntimeRestorePending(); - PersistentPhysicsWorldResource failed = new PersistentPhysicsWorldResource(); - failed.markRuntimeRestorePending(); - failed.failRuntimeRestore("test failure"); - - assertFalse(PhysicsStepRestoreGate.canSubmitStep(pending)); - assertFalse(PhysicsStepRestoreGate.canSubmitStep(failed)); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepSystemTest.java deleted file mode 100644 index 8267974d..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/step/PhysicsStepSystemTest.java +++ /dev/null @@ -1,631 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.step; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBackend; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsContact; -import dev.hytalemodding.impulse.api.PhysicsJoint; -import dev.hytalemodding.impulse.api.PhysicsRayHit; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.resources.profiling.PhysicsRuntimeProfilingResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.TestPhysicsOwnerLane; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerResource; -import dev.hytalemodding.impulse.core.internal.resources.owner.PhysicsOwnerStepCompletion; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsStepSchedulingMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import dev.hytalemodding.impulse.core.plugin.snapshot.PublishedPhysicsSnapshotFrame; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; -import javax.annotation.Nonnull; -import org.junit.jupiter.api.Test; -import org.joml.Vector3f; - -class PhysicsStepSystemTest { - - private static final AtomicInteger BACKEND_COUNTER = new AtomicInteger(); - - @Test - void submittedStepKeepsSchedulerSequenceSeparateFromServerTick() throws Exception { - PhysicsStepSystem system = new PhysicsStepSystem(); - PhysicsStepSystem.StepSchedulerState state = new PhysicsStepSystem.StepSchedulerState(); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsRuntimeProfilingResource profiling = new PhysicsRuntimeProfilingResource(); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(2, - Duration.ofSeconds(2L))) { - owner.start("step-system-sequence-test"); - resource.attachOwnerExecutor(owner); - - // Owner steps run detached from the scheduling tick, so the published - // frame must preserve the scheduling metadata captured at submission. - system.submitStepIfIdle(state, owner, resource, 0.05f, profiling, 42L); - - PhysicsOwnerStepCompletion completion = pollCompletedStep(owner); - PublishedPhysicsSnapshotFrame frame = completion.frame(); - assertNotNull(frame); - assertEquals(1L, frame.stepSequence()); - assertEquals(42L, frame.serverTick()); - } - } - - @Test - void pendingOwnerStepAccumulatesElapsedDtForNextSubmission() throws Exception { - PhysicsStepSystem system = new PhysicsStepSystem(); - PhysicsStepSystem.StepSchedulerState state = new PhysicsStepSystem.StepSchedulerState(); - RecordingBackend backend = registerBackend(); - LegacyLiveHandleTestResource resource = fixedStepResource(backend); - configureWorldSettings(resource, settings -> { - settings.setStepSchedulingMode(PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT); - settings.setStepMode(PhysicsStepMode.PROGRESSIVE_REFINEMENT); - settings.setMaxStepDt(1.0f); - }); - PhysicsRuntimeProfilingResource profiling = new PhysicsRuntimeProfilingResource(); - RecordingSpace space = backend.space(); - CountDownLatch stepStarted = new CountDownLatch(1); - CountDownLatch releaseStep = new CountDownLatch(1); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(2, - Duration.ofSeconds(2L))) { - owner.start("step-system-accumulated-dt-test"); - resource.attachOwnerExecutor(owner); - space.blockNextStep(stepStarted, releaseStep); - - system.submitStepIfIdle(state, owner, resource, 0.05f, profiling, 1L); - assertTrue(stepStarted.await(2, TimeUnit.SECONDS)); - - system.submitStepIfIdle(state, owner, resource, 0.05f, profiling, 2L); - system.submitStepIfIdle(state, owner, resource, 0.05f, profiling, 3L); - - releaseStep.countDown(); - pollCompletedStep(owner); - - system.submitStepIfIdle(state, owner, resource, 0.05f, profiling, 4L); - PhysicsOwnerStepCompletion completion = pollCompletedStep(owner); - - assertEquals(2, space.stepDts.size()); - assertEquals(0.05f, space.stepDts.get(0), 0.00001f); - assertEquals(0.15f, space.stepDts.get(1), 0.00001f); - PublishedPhysicsSnapshotFrame frame = completion.frame(); - assertNotNull(frame); - assertEquals(2L, frame.stepSequence()); - assertEquals(4L, frame.serverTick()); - } finally { - releaseStep.countDown(); - } - } - - @Test - void accumulatedDtIsCappedAndDroppedBacklogIsProfiled() throws Exception { - PhysicsStepSystem system = new PhysicsStepSystem(); - PhysicsStepSystem.StepSchedulerState state = new PhysicsStepSystem.StepSchedulerState(); - RecordingBackend backend = registerBackend(); - LegacyLiveHandleTestResource resource = fixedStepResource(backend); - configureWorldSettings(resource, settings -> { - settings.setStepSchedulingMode(PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT); - settings.setMaxStepDt(0.05f); - }); - PhysicsRuntimeProfilingResource profiling = new PhysicsRuntimeProfilingResource(); - profiling.setEnabled(true); - RecordingSpace space = backend.space(); - CountDownLatch stepStarted = new CountDownLatch(1); - CountDownLatch releaseStep = new CountDownLatch(1); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(2, - Duration.ofSeconds(2L))) { - owner.start("step-system-capped-dt-test"); - resource.attachOwnerExecutor(owner); - space.blockNextStep(stepStarted, releaseStep); - - system.submitStepIfIdle(state, owner, resource, 0.05f, profiling, 1L); - assertTrue(stepStarted.await(2, TimeUnit.SECONDS)); - for (int tick = 2; tick < 12; tick++) { - system.submitStepIfIdle(state, owner, resource, 0.05f, profiling, tick); - } - - releaseStep.countDown(); - pollCompletedStep(owner); - - system.submitStepIfIdle(state, owner, resource, 0.0f, profiling, 12L); - pollCompletedStep(owner); - - assertEquals(2, space.stepDts.size()); - assertEquals(0.05f, space.stepDts.get(1), 0.00001f); - assertTrue(profiling.getCumulativeStep().getDtCapHits() > 0); - assertTrue(profiling.getCumulativeStep().getDroppedBacklogTicks() > 0); - assertTrue(profiling.getCumulativeStep().getDroppedBacklogDtNanos() > 0L); - long maxBacklogDtNanos = - profiling.getCumulativeStep().getMaxSchedulerBacklogDtNanos(); - assertTrue(Math.abs(maxBacklogDtNanos - 50_000_000L) <= 1L); - } finally { - releaseStep.countDown(); - } - } - - @Test - void fixedAndCcdAccumulatedDtCapUsesConfiguredStepBudget() { - LegacyLiveHandleTestResource fixed = new LegacyLiveHandleTestResource(); - configureWorldSettings(fixed, settings -> { - settings.setStepMode(PhysicsStepMode.FIXED); - settings.setSimulationSteps(3); - settings.setMaxStepDt(0.02f); - }); - - LegacyLiveHandleTestResource ccd = new LegacyLiveHandleTestResource(); - configureWorldSettings(ccd, settings -> { - settings.setStepMode(PhysicsStepMode.CCD); - settings.setSimulationSteps(2); - settings.setMaxStepDt(0.02f); - }); - - assertEquals(0.06f, PhysicsStepSystem.maxAccumulatedStepDt(fixed), 0.00001f); - assertEquals(0.04f, PhysicsStepSystem.maxAccumulatedStepDt(ccd), 0.00001f); - } - - @Test - void nonFinitePendingDtDoesNotPoisonAccumulator() throws Exception { - PhysicsStepSystem system = new PhysicsStepSystem(); - PhysicsStepSystem.StepSchedulerState state = new PhysicsStepSystem.StepSchedulerState(); - RecordingBackend backend = registerBackend(); - LegacyLiveHandleTestResource resource = fixedStepResource(backend); - configureWorldSettings(resource, settings -> { - settings.setStepSchedulingMode(PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT); - settings.setMaxStepDt(0.05f); - }); - PhysicsRuntimeProfilingResource profiling = new PhysicsRuntimeProfilingResource(); - RecordingSpace space = backend.space(); - CountDownLatch stepStarted = new CountDownLatch(1); - CountDownLatch releaseStep = new CountDownLatch(1); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(2, - Duration.ofSeconds(2L))) { - owner.start("step-system-safe-dt-test"); - resource.attachOwnerExecutor(owner); - space.blockNextStep(stepStarted, releaseStep); - - system.submitStepIfIdle(state, owner, resource, 0.05f, profiling, 1L); - assertTrue(stepStarted.await(2, TimeUnit.SECONDS)); - system.submitStepIfIdle(state, owner, resource, Float.NaN, profiling, 2L); - system.submitStepIfIdle(state, owner, resource, Float.POSITIVE_INFINITY, profiling, 3L); - system.submitStepIfIdle(state, owner, resource, -1.0f, profiling, 4L); - system.submitStepIfIdle(state, owner, resource, 0.05f, profiling, 5L); - - releaseStep.countDown(); - pollCompletedStep(owner); - - system.submitStepIfIdle(state, owner, resource, 0.0f, profiling, 6L); - pollCompletedStep(owner); - - assertEquals(2, space.stepDts.size()); - assertEquals(0.05f, space.stepDts.get(1), 0.00001f); - } finally { - releaseStep.countDown(); - } - } - - @Test - void dropPendingDtPolicySubmitsOnlyCurrentTickDtAfterPendingStep() throws Exception { - PhysicsStepSystem system = new PhysicsStepSystem(); - PhysicsStepSystem.StepSchedulerState state = new PhysicsStepSystem.StepSchedulerState(); - RecordingBackend backend = registerBackend(); - LegacyLiveHandleTestResource resource = fixedStepResource(backend); - configureWorldSettings(resource, - settings -> settings.setStepSchedulingMode(PhysicsStepSchedulingMode.DROP_PENDING_DT)); - PhysicsRuntimeProfilingResource profiling = new PhysicsRuntimeProfilingResource(); - profiling.setEnabled(true); - RecordingSpace space = backend.space(); - CountDownLatch stepStarted = new CountDownLatch(1); - CountDownLatch releaseStep = new CountDownLatch(1); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(2, - Duration.ofSeconds(2L))) { - owner.start("step-system-drop-pending-dt-test"); - resource.attachOwnerExecutor(owner); - space.blockNextStep(stepStarted, releaseStep); - - system.submitStepIfIdle(state, owner, resource, 0.05f, profiling, 1L); - assertTrue(stepStarted.await(2, TimeUnit.SECONDS)); - - system.submitStepIfIdle(state, owner, resource, 0.05f, profiling, 2L); - system.submitStepIfIdle(state, owner, resource, 0.05f, profiling, 3L); - - releaseStep.countDown(); - pollCompletedStep(owner); - - system.submitStepIfIdle(state, owner, resource, 0.05f, profiling, 4L); - PhysicsOwnerStepCompletion completion = pollCompletedStep(owner); - - assertEquals(2, space.stepDts.size()); - assertEquals(0.05f, space.stepDts.get(0), 0.00001f); - assertEquals(0.05f, space.stepDts.get(1), 0.00001f); - assertEquals(0, profiling.getCumulativeStep().getSchedulerSamples()); - assertEquals(2, profiling.getCumulativeStep().getSkippedPendingSteps()); - PublishedPhysicsSnapshotFrame frame = completion.frame(); - assertNotNull(frame); - assertEquals(2L, frame.stepSequence()); - assertEquals(4L, frame.serverTick()); - } finally { - releaseStep.countDown(); - } - } - - @Test - void restoreGuardSkipsStepSubmissionAndClearsAccumulatedDt() throws Exception { - PhysicsStepSystem system = new PhysicsStepSystem(); - PhysicsStepSystem.StepSchedulerState state = new PhysicsStepSystem.StepSchedulerState(); - RecordingBackend backend = registerBackend(); - LegacyLiveHandleTestResource resource = fixedStepResource(backend); - configureWorldSettings(resource, settings -> { - settings.setStepSchedulingMode(PhysicsStepSchedulingMode.ACCUMULATE_PENDING_DT); - settings.setMaxStepDt(1.0f); - }); - PhysicsRuntimeProfilingResource profiling = new PhysicsRuntimeProfilingResource(); - PersistentPhysicsWorldResource persistent = new PersistentPhysicsWorldResource(); - RecordingSpace space = backend.space(); - CountDownLatch stepStarted = new CountDownLatch(1); - CountDownLatch releaseStep = new CountDownLatch(1); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(2, - Duration.ofSeconds(2L))) { - owner.start("step-system-restore-guard-test"); - resource.attachOwnerExecutor(owner); - space.blockNextStep(stepStarted, releaseStep); - - system.submitStepIfIdle(state, owner, resource, 0.05f, profiling, 1L); - assertTrue(stepStarted.await(2, TimeUnit.SECONDS)); - system.submitStepIfIdle(state, owner, resource, 0.05f, profiling, 2L); - - persistent.markRuntimeRestorePending(); - assertFalse(system.submitStepIfRestoreReady(state, - persistent, - owner, - resource, - 0.05f, - profiling, - 3L)); - - releaseStep.countDown(); - pollCompletedStep(owner); - - system.submitStepIfIdle(state, owner, resource, 0.0f, profiling, 4L); - pollCompletedStep(owner); - - assertEquals(2, space.stepDts.size()); - assertEquals(0.05f, space.stepDts.get(0), 0.00001f); - assertEquals(0.0f, space.stepDts.get(1), 0.00001f); - } finally { - releaseStep.countDown(); - } - } - - @Test - void failedRestoreGuardSkipsStepSubmission() { - PhysicsStepSystem system = new PhysicsStepSystem(); - PhysicsStepSystem.StepSchedulerState state = new PhysicsStepSystem.StepSchedulerState(); - RecordingBackend backend = registerBackend(); - LegacyLiveHandleTestResource resource = fixedStepResource(backend); - PhysicsRuntimeProfilingResource profiling = new PhysicsRuntimeProfilingResource(); - PersistentPhysicsWorldResource persistent = new PersistentPhysicsWorldResource(); - persistent.failRuntimeRestore("bad persisted state"); - - try (TestPhysicsOwnerLane owner = new TestPhysicsOwnerLane(2, - Duration.ofSeconds(2L))) { - assertFalse(system.submitStepIfRestoreReady(state, - persistent, - owner, - resource, - 0.05f, - profiling, - 1L)); - assertFalse(owner.hasPendingStep()); - } - } - - @Nonnull - private static RecordingBackend registerBackend() { - RecordingBackend backend = new RecordingBackend("test:step-system-" - + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(backend); - return backend; - } - - @Nonnull - private static LegacyLiveHandleTestResource fixedStepResource(@Nonnull RecordingBackend backend) { - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - configureWorldSettings(resource, settings -> { - settings.setStepMode(PhysicsStepMode.FIXED); - settings.setSimulationSteps(1); - }); - resource.createSpace(backend.getId(), - "step-system-test", - PhysicsSpaceSettings.defaults()); - return resource; - } - - private static void configureWorldSettings(@Nonnull LegacyLiveHandleTestResource resource, - @Nonnull Consumer configurator) { - PhysicsWorldSettings settings = resource.getWorldSettings(); - configurator.accept(settings); - resource.setWorldSettings(settings); - } - - @Nonnull - private static PhysicsOwnerStepCompletion pollCompletedStep( - @Nonnull PhysicsOwnerResource owner) throws InterruptedException { - long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(2L); - while (System.nanoTime() < deadline) { - PhysicsOwnerStepCompletion completion = owner.pollCompletedStep(); - if (completion != null) { - return completion; - } - Thread.sleep(10L); - } - fail("Timed out waiting for physics step completion"); - throw new AssertionError(); - } - - private static final class RecordingBackend implements PhysicsBackend { - - @Nonnull - private final BackendId id; - private RecordingSpace space; - - private RecordingBackend(@Nonnull String id) { - this.id = new BackendId(id); - } - - @Nonnull - @Override - public BackendId getId() { - return id; - } - - @Override - public void init() { - } - - @Nonnull - @Override - public PhysicsSpace createSpace() { - return createSpace(SpaceId.next()); - } - - @Nonnull - @Override - public PhysicsSpace createSpace(@Nonnull SpaceId spaceId) { - space = new RecordingSpace(spaceId, id); - return space; - } - - @Nonnull - private RecordingSpace space() { - return space; - } - } - - private static final class RecordingSpace implements PhysicsSpace { - - @Nonnull - private final SpaceId id; - @Nonnull - private final BackendId backendId; - @Nonnull - private final Vector3f gravity = new Vector3f(); - @Nonnull - private final List stepDts = new ArrayList<>(); - @Nonnull - private CountDownLatch stepStarted = new CountDownLatch(0); - @Nonnull - private CountDownLatch releaseStep = new CountDownLatch(0); - - private RecordingSpace(@Nonnull SpaceId id, @Nonnull BackendId backendId) { - this.id = id; - this.backendId = backendId; - } - - private void blockNextStep(@Nonnull CountDownLatch stepStarted, - @Nonnull CountDownLatch releaseStep) { - this.stepStarted = stepStarted; - this.releaseStep = releaseStep; - } - - @Nonnull - @Override - public SpaceId id() { - return id; - } - - @Nonnull - @Override - public BackendId backendId() { - return backendId; - } - - @Override - public void step(float dt) { - stepDts.add(dt); - stepStarted.countDown(); - try { - releaseStep.await(2, TimeUnit.SECONDS); - } catch (InterruptedException exception) { - Thread.currentThread().interrupt(); - throw new IllegalStateException("Interrupted while blocking physics step", exception); - } finally { - stepStarted = new CountDownLatch(0); - releaseStep = new CountDownLatch(0); - } - } - - @Override - public void setGravity(float x, float y, float z) { - gravity.set(x, y, z); - } - - @Nonnull - @Override - public Vector3f getGravity() { - return new Vector3f(gravity); - } - - @Override - public void addBody(@Nonnull PhysicsBody body) { - } - - @Override - public void removeBody(@Nonnull PhysicsBody body) { - } - - @Nonnull - @Override - public List getBodies() { - return List.of(); - } - - @Nonnull - @Override - public PhysicsBody createStaticPlane(float groundY) { - throw new UnsupportedOperationException(); - } - - @Nonnull - @Override - public PhysicsBody createBox(float halfX, float halfY, float halfZ, float mass) { - throw new UnsupportedOperationException(); - } - - @Nonnull - @Override - public PhysicsBody createBox(@Nonnull Vector3f halfExtents, float mass) { - throw new UnsupportedOperationException(); - } - - @Nonnull - @Override - public PhysicsBody createSphere(float radius, float mass) { - throw new UnsupportedOperationException(); - } - - @Nonnull - @Override - public PhysicsBody createCapsule(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - throw new UnsupportedOperationException(); - } - - @Nonnull - @Override - public PhysicsBody createCylinder(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - throw new UnsupportedOperationException(); - } - - @Nonnull - @Override - public PhysicsBody createCone(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - throw new UnsupportedOperationException(); - } - - @Nonnull - @Override - public Optional raycastClosest(@Nonnull Vector3f from, @Nonnull Vector3f to) { - return Optional.empty(); - } - - @Nonnull - @Override - public List raycastAll(@Nonnull Vector3f from, @Nonnull Vector3f to) { - return List.of(); - } - - @Nonnull - @Override - public List getContacts() { - return List.of(); - } - - @Nonnull - @Override - public PhysicsJoint createFixedJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB) { - throw new UnsupportedOperationException(); - } - - @Nonnull - @Override - public PhysicsJoint createPointJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB) { - throw new UnsupportedOperationException(); - } - - @Nonnull - @Override - public PhysicsJoint createHingeJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis) { - throw new UnsupportedOperationException(); - } - - @Nonnull - @Override - public PhysicsJoint createSliderJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis) { - throw new UnsupportedOperationException(); - } - - @Nonnull - @Override - public PhysicsJoint createSpringJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - float restLength, - float stiffness, - float damping) { - throw new UnsupportedOperationException(); - } - - @Override - public void removeJoint(@Nonnull PhysicsJoint joint) { - } - - @Nonnull - @Override - public List getJoints() { - return List.of(); - } - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsSyncSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsSyncSystemTest.java deleted file mode 100644 index 0dcbd896..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/sync/PhysicsSyncSystemTest.java +++ /dev/null @@ -1,145 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.sync; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.math.PhysicsVisualPoseMath; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent.AttachmentLifecycle; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent.TransformAuthority; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import org.joml.Quaterniond; -import org.joml.Quaternionf; -import org.joml.Vector3d; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class PhysicsSyncSystemTest { - - @Test - void visualPredictionSecondsClampToConfiguredWindow() { - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - settings.getVisualSyncSettings().setVisualSnapshotPredictionEnabled(true); - settings.getVisualSyncSettings().setVisualSnapshotPredictionMaxSeconds(0.05f); - - assertEquals(0.05f, - PhysicsSyncPolicy.visualPredictionSeconds(settings, - 1_100_000_000L, - 1_000_000_000L), - 0.0001f); - } - - @Test - void visualPredictionSecondsStayZeroWhenDisabledOrMissingFrame() { - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - settings.getVisualSyncSettings().setVisualSnapshotPredictionEnabled(true); - - assertEquals(0.0f, - PhysicsSyncPolicy.visualPredictionSeconds(settings, 1_100_000_000L, 0L), - 0.0001f); - settings.getVisualSyncSettings().setVisualSnapshotPredictionEnabled(false); - assertEquals(0.0f, - PhysicsSyncPolicy.visualPredictionSeconds(settings, - 1_100_000_000L, - 1_000_000_000L), - 0.0001f); - } - - @Test - void bodyTransformSyncOnlyAppliesToBodyAuthoritativeAttachments() { - RigidBodyKey bodyKey = RigidBodyKey.random(); - SpaceId spaceId = new SpaceId(1); - - assertTrue(PhysicsTransformAuthority.shouldApplyBodyTransform(new PhysicsBodyAttachmentComponent(bodyKey, - spaceId, - TransformAuthority.BODY, - AttachmentLifecycle.EXTERNAL_ENTITY))); - assertFalse(PhysicsTransformAuthority.shouldApplyBodyTransform(new PhysicsBodyAttachmentComponent(bodyKey, - spaceId, - TransformAuthority.CONTROLLER, - AttachmentLifecycle.EXTERNAL_ENTITY))); - assertFalse(PhysicsTransformAuthority.shouldApplyBodyTransform(new PhysicsBodyAttachmentComponent(bodyKey, - spaceId, - TransformAuthority.ENTITY_KINEMATIC, - AttachmentLifecycle.EXTERNAL_ENTITY))); - } - - @Test - void visualPositionKeepsCenterOfMassOffsetWorldUp() { - Vector3f visualPosition = PhysicsVisualPoseMath.visualPositionFromBodyPose(new Vector3f(10.0f, - 20.0f, - 30.0f), - new Quaternionf().rotateZ((float) (Math.PI / 2.0)), - 0.5f, - new Vector3f(), - new Vector3f()); - - assertEquals(10.0f, visualPosition.x, 0.0001f); - assertEquals(19.5f, visualPosition.y, 0.0001f); - assertEquals(30.0f, visualPosition.z, 0.0001f); - } - - @Test - void visualPositionRotatesLocalAttachmentOffset() { - Vector3f visualPosition = PhysicsVisualPoseMath.visualPositionFromBodyPose(new Vector3f(10.0f, - 20.0f, - 30.0f), - new Quaternionf().rotateZ((float) (Math.PI / 2.0)), - 0.5f, - new Vector3f(1.0f, 0.0f, 0.0f), - new Vector3f()); - - assertEquals(10.0f, visualPosition.x, 0.0001f); - assertEquals(20.5f, visualPosition.y, 0.0001f); - assertEquals(30.0f, visualPosition.z, 0.0001f); - } - - @Test - void bodyCenterInvertsWorldUpCenterOfMassOffsetAndRotatedLocalOffset() { - Vector3f bodyCenter = new Vector3f(10.0f, 20.0f, 30.0f); - Quaternionf bodyRotation = new Quaternionf().rotateZ((float) (Math.PI / 2.0)); - float centerOfMassOffsetY = 0.5f; - Vector3f visualPosition = PhysicsVisualPoseMath.visualPositionFromBodyPose(bodyCenter, - bodyRotation, - centerOfMassOffsetY, - new Vector3f(1.0f, 0.0f, 0.0f), - new Vector3f()); - - Vector3d invertedCenter = PhysicsVisualPoseMath.bodyCenterFromVisualPose( - new Vector3d(visualPosition.x, visualPosition.y, visualPosition.z), - new Quaterniond(bodyRotation), - centerOfMassOffsetY, - new Vector3f(1.0f, 0.0f, 0.0f), - new Vector3d()); - - assertEquals(bodyCenter.x, invertedCenter.x, 0.0001f); - assertEquals(bodyCenter.y, invertedCenter.y, 0.0001f); - assertEquals(bodyCenter.z, invertedCenter.z, 0.0001f); - } - - @Test - void attachmentVisualOriginOffsetOverridesBodyShapeOffset() { - PhysicsBodyAttachmentComponent attachment = PhysicsBodyAttachmentComponent.externalEntity( - RigidBodyKey.random(), - null, - new Vector3f(0.0f, -0.5f, 0.0f), - new Quaternionf(), - 0.5f); - - Vector3f visualPosition = PhysicsVisualPoseMath.visualPositionFromBodyPose(new Vector3f(10.0f, - 20.0f, - 30.0f), - new Quaternionf(), - attachment.resolveVisualOriginOffsetY(1.0f), - attachment.getLocalPositionOffset(), - new Vector3f()); - - assertEquals(10.0f, visualPosition.x, 0.0001f); - assertEquals(19.0f, visualPosition.y, 0.0001f); - assertEquals(30.0f, visualPosition.z, 0.0001f); - assertEquals(0.5f, attachment.clone().getVisualOriginOffsetY(), 0.0001f); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/visual/PhysicsDetachedVisualMaterializationSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/visual/PhysicsDetachedVisualMaterializationSystemTest.java deleted file mode 100644 index 7faa6624..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/visual/PhysicsDetachedVisualMaterializationSystemTest.java +++ /dev/null @@ -1,226 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.systems.visual; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.Impulse; -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackend; -import dev.hytalemodding.impulse.core.internal.persistence.PersistentPhysicsWorldResource; -import dev.hytalemodding.impulse.core.internal.testsupport.LegacyLiveHandleTestResource; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsVisualRuntime; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.VisualOcclusionMode; -import java.util.List; -import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import org.joml.Quaternionf; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class PhysicsDetachedVisualMaterializationSystemTest { - - private static final AtomicInteger BACKEND_COUNTER = new AtomicInteger(); - - @Test - void materializationTreatsEcsGameplayAttachmentSnapshotAsAttachmentWhenRuntimeIndexIsStale() { - RigidBodyKey bodyKey = RigidBodyKey.random(); - RigidBodyKey otherBodyKey = RigidBodyKey.random(); - AtomicBoolean queriedSnapshot = new AtomicBoolean(); - - GameplayAttachmentSnapshot snapshot = GameplayAttachmentSnapshot.fromSource(() -> Set.of(bodyKey)); - assertTrue(snapshot.hasKnownGameplayAttachment(false, bodyKey)); - - GameplayAttachmentSnapshot currentRuntimeIndex = GameplayAttachmentSnapshot.fromSource( - () -> { - queriedSnapshot.set(true); - return Set.of(); - }); - assertTrue(currentRuntimeIndex.hasKnownGameplayAttachment(true, bodyKey)); - assertFalse(queriedSnapshot.get()); - - GameplayAttachmentSnapshot otherSnapshot = GameplayAttachmentSnapshot.fromSource(() -> Set.of(otherBodyKey)); - assertFalse(otherSnapshot.hasKnownGameplayAttachment(false, bodyKey)); - } - - @Test - void impulseOwnedVisualAttachmentIsDisposableWhenBodyMissing() { - PhysicsBodyAttachmentComponent ownedVisual = PhysicsBodyAttachmentComponent.impulseOwnedVisual( - RigidBodyKey.random(), - null, - new Vector3f(), - new Quaternionf(), - 0.5f); - PhysicsBodyAttachmentComponent gameplayEntity = PhysicsBodyAttachmentComponent.externalEntity( - RigidBodyKey.random(), - null); - - assertTrue(ownedVisual.shouldRemoveEntityWhenBodyMissing()); - assertFalse(gameplayEntity.shouldRemoveEntityWhenBodyMissing()); - } - - @Test - void scalarVisibleDistanceRespectsRadiusAndViewCone() { - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - settings.getVisualSyncSettings().setVisualVisibilityCullingEnabled(true); - List interests = List.of( - new PhysicsVisualRuntime.VisualInterest(new Vector3f(), new Vector3f(1.0f, 0.0f, 0.0f))); - - assertEquals(9.0f, - DetachedVisualGeometry.visibleDistanceSquared(3.0f, - 0.0f, - 0.0f, - settings, - interests, - 10.0f)); - assertEquals(Float.POSITIVE_INFINITY, - DetachedVisualGeometry.visibleDistanceSquared(-9.0f, - 0.0f, - 0.0f, - settings, - interests, - 10.0f)); - assertEquals(16.0f, - DetachedVisualGeometry.visibleDistanceSquared(-4.0f, - 0.0f, - 0.0f, - settings, - interests, - 10.0f)); - assertEquals(Float.POSITIVE_INFINITY, - DetachedVisualGeometry.visibleDistanceSquared(11.0f, - 0.0f, - 0.0f, - settings, - interests, - 10.0f)); - } - - @Test - void cachedMaterializationPolicyRejectsCurrentPoseOutsideRadius() { - BackendId backendId = new BackendId("test:visual-policy-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(new FakePhysicsBackend(backendId)); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - settings.getVisualMaterializationSettings().setDetachedVisualMaterializationEnabled(true); - settings.getVisualMaterializationSettings().setDetachedVisualRadii(4, 8); - PhysicsSpace space = resource.createLiveSpace(backendId, "test-world", settings); - List interests = List.of( - new PhysicsVisualRuntime.VisualInterest(new Vector3f(), null)); - - DetachedVisualOcclusion.Result result = - PhysicsDetachedVisualMaterializationSystem.resolveCurrentMaterializationPolicy(resource, - RigidBodyKey.random(), - resource.requireSpaceBinding(space.id()), - snapshotAt(9.0f, 0.0f, 0.0f), - settings, - interests, - 1L, - new DetachedVisualOcclusion.RaycastBudget(), - null); - - assertFalse(result.shouldMaterialize()); - assertEquals(Float.POSITIVE_INFINITY, result.distanceSquared()); - } - - @Test - void failedRestoreBlocksDetachedVisualMaterialization() { - PersistentPhysicsWorldResource persistent = new PersistentPhysicsWorldResource(); - - assertFalse(PhysicsDetachedVisualMaterializationSystem.isRestoreBlockingVisuals(persistent)); - - persistent.failRuntimeRestore("bad saved backend"); - - assertTrue(PhysicsDetachedVisualMaterializationSystem.isRestoreBlockingVisuals(persistent)); - } - - @Test - void firstOcclusionFrameKeepsCandidateVisibleWhileRaycastIsPending() { - BackendId backendId = new BackendId("test:visual-occlusion-" + BACKEND_COUNTER.incrementAndGet()); - Impulse.registerBackend(new FakePhysicsBackend(backendId)); - LegacyLiveHandleTestResource resource = new LegacyLiveHandleTestResource(); - PhysicsSpaceSettings settings = PhysicsSpaceSettings.defaults(); - settings.getVisualSyncSettings().setVisualOcclusionMode(VisualOcclusionMode.CULL); - settings.getVisualSyncSettings().setVisualOcclusionRaycastsPerTick(1); - PhysicsSpace space = resource.createLiveSpace(backendId, "test-world", settings); - RigidBodyKey bodyKey = RigidBodyKey.random(); - PhysicsBodySnapshot snapshot = PhysicsBodySnapshot.of(0.0f, - 0.0f, - 4.0f, - 0.0f, - 0.0f, - 0.0f, - 1.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - PhysicsBodyType.DYNAMIC, - false, - false, - 0.0f, - ShapeType.BOX, - true, - 0.5f, - 0.5f, - 0.5f, - 0.0f, - 0.0f, - PhysicsAxis.Y); - List interests = List.of( - new PhysicsVisualRuntime.VisualInterest(new Vector3f(), null)); - - DetachedVisualOcclusion.Result result = assertDoesNotThrow(() -> DetachedVisualOcclusion.resolve(resource, - bodyKey, - resource.requireSpaceBinding(space.id()), - snapshot, - settings, - interests, - 16.0f, - 1L, - new DetachedVisualOcclusion.RaycastBudget(), - null)); - - assertTrue(result.shouldMaterialize()); - } - - private static PhysicsBodySnapshot snapshotAt(float x, float y, float z) { - return PhysicsBodySnapshot.of(x, - y, - z, - 0.0f, - 0.0f, - 0.0f, - 1.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - PhysicsBodyType.DYNAMIC, - false, - false, - 0.0f, - ShapeType.BOX, - true, - 0.5f, - 0.5f, - 0.5f, - 0.0f, - 0.0f, - PhysicsAxis.Y); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/visual/PhysicsProjectionCleanupSystemTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/visual/PhysicsProjectionCleanupSystemTest.java new file mode 100644 index 00000000..c225c624 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/systems/visual/PhysicsProjectionCleanupSystemTest.java @@ -0,0 +1,227 @@ +package dev.hytalemodding.impulse.core.internal.systems.visual; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.PhysicsEntityTypeRegistry; +import dev.hytalemodding.impulse.core.internal.modules.physicsentity.systems.visual.PhysicsProjectionCleanupSystem; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsProjectionIndexResource; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent.AttachmentLifecycle; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.GeneratedVisualProxyComponent; +import java.util.ArrayList; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +class PhysicsProjectionCleanupSystemTest { + + @AfterEach + void clearTypes() { + PhysicsEntityTypeRegistry.clearEntityStoreTypes(); + } + + @Test + void externalAttachmentWithDestroyedBodyRefIsDetachedButEntityRemains() { + ComponentRegistry registry = new ComponentRegistry<>(); + Store store = store(registry, "projection-cleanup-external"); + try { + UUID bodyUuid = UUID.randomUUID(); + Ref bodyRef = new TestPhysicsRef(11, false); + Ref entityRef = addAttachment(store, + bodyUuid, + bodyRef, + AttachmentLifecycle.EXTERNAL_ENTITY, + false); + PhysicsProjectionIndexResource projection = projection(store); + projection.registerAttachment(bodyUuid, bodyRef, entityRef); + + new PhysicsProjectionCleanupSystem().tick(0.0f, 0, store); + + assertTrue(entityRef.isValid()); + assertNull(store.getComponent(entityRef, BodyAttachmentComponent.getComponentType())); + assertFalse(projection.hasAttachments(bodyUuid)); + assertFalse(projection.hasAttachments(bodyRef)); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void generatedProxyWithDestroyedBodyRefIsRemovedAndUnindexed() { + ComponentRegistry registry = new ComponentRegistry<>(); + Store store = store(registry, "projection-cleanup-generated"); + try { + UUID bodyUuid = UUID.randomUUID(); + Ref bodyRef = new TestPhysicsRef(12, false); + Ref proxyRef = addAttachment(store, + bodyUuid, + bodyRef, + AttachmentLifecycle.GENERATED_PROXY, + true); + PhysicsProjectionIndexResource projection = projection(store); + projection.registerAttachment(bodyUuid, bodyRef, proxyRef); + projection.setGeneratedVisualProxy(bodyUuid, bodyRef, proxyRef); + + new PhysicsProjectionCleanupSystem().tick(0.0f, 0, store); + + assertFalse(proxyRef.isValid()); + assertFalse(projection.hasAttachments(bodyUuid)); + assertFalse(projection.hasAttachments(bodyRef)); + assertNull(projection.getGeneratedVisualProxy(bodyUuid)); + assertNull(projection.getGeneratedVisualProxy(bodyRef)); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void generatedProxyWithMissingBodyRefIsPreservedWithoutPhysicsStoreAuthority() { + ComponentRegistry registry = new ComponentRegistry<>(); + Store store = store(registry, "projection-cleanup-generated-missing-ref"); + try { + UUID bodyUuid = UUID.randomUUID(); + Ref proxyRef = addAttachment(store, + bodyUuid, + null, + AttachmentLifecycle.GENERATED_PROXY, + true); + PhysicsProjectionIndexResource projection = projection(store); + projection.registerAttachment(bodyUuid, proxyRef); + projection.setGeneratedVisualProxy(bodyUuid, proxyRef); + + new PhysicsProjectionCleanupSystem().tick(0.0f, 0, store); + + assertTrue(proxyRef.isValid()); + assertNotNull(store.getComponent(proxyRef, BodyAttachmentComponent.getComponentType())); + assertTrue(projection.hasAttachments(bodyUuid)); + assertNotNull(projection.getGeneratedVisualProxy(bodyUuid)); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void externalAttachmentWithMissingBodyRefIsPreservedForUuidResolution() { + ComponentRegistry registry = new ComponentRegistry<>(); + Store store = store(registry, "projection-cleanup-external-missing-ref"); + try { + UUID bodyUuid = UUID.randomUUID(); + Ref entityRef = addAttachment(store, + bodyUuid, + null, + AttachmentLifecycle.EXTERNAL_ENTITY, + false); + PhysicsProjectionIndexResource projection = projection(store); + projection.registerAttachment(bodyUuid, entityRef); + + new PhysicsProjectionCleanupSystem().tick(0.0f, 0, store); + + assertTrue(entityRef.isValid()); + assertNotNull(store.getComponent(entityRef, BodyAttachmentComponent.getComponentType())); + assertTrue(projection.hasAttachments(bodyUuid)); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void validBodyRefIsPreservedWhenSnapshotPublicationLags() { + ComponentRegistry registry = new ComponentRegistry<>(); + Store store = store(registry, "projection-cleanup-valid-ref"); + try { + UUID bodyUuid = UUID.randomUUID(); + Ref bodyRef = new TestPhysicsRef(13, true); + Ref entityRef = addAttachment(store, + bodyUuid, + bodyRef, + AttachmentLifecycle.EXTERNAL_ENTITY, + false); + PhysicsProjectionIndexResource projection = projection(store); + projection.registerAttachment(bodyUuid, bodyRef, entityRef); + + new PhysicsProjectionCleanupSystem().tick(0.0f, 0, store); + + assertTrue(entityRef.isValid()); + assertNotNull(store.getComponent(entityRef, BodyAttachmentComponent.getComponentType())); + assertTrue(projection.hasAttachments(bodyUuid)); + assertTrue(projection.hasAttachments(bodyRef)); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Nonnull + private static Store store(@Nonnull ComponentRegistry registry, + @Nonnull String worldName) { + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsEntityTypeRegistry.registerComponentTypes(proxy); + PhysicsEntityTypeRegistry.registerResourceTypes(proxy); + PhysicsEntityTypeRegistry.registerSystemGroups(proxy); + Store store = registry.addStore( + new EntityStore(TestInstanceFactory.world(worldName)), + EmptyResourceStorage.get()); + return store; + } + + @Nonnull + private static Ref addAttachment(@Nonnull Store store, + @Nonnull UUID bodyUuid, + @Nullable Ref bodyRef, + @Nonnull AttachmentLifecycle lifecycle, + boolean generatedProxy) { + Holder holder = store.getRegistry().newHolder(); + BodyAttachmentComponent attachment = new BodyAttachmentComponent(bodyUuid, + BodyAttachmentComponent.TransformAuthority.BODY, + lifecycle); + attachment.setBodyRef(bodyRef); + holder.addComponent(BodyAttachmentComponent.getComponentType(), attachment); + if (generatedProxy) { + holder.addComponent(GeneratedVisualProxyComponent.getComponentType(), + new GeneratedVisualProxyComponent()); + } + Ref ref = store.addEntity(holder, AddReason.SPAWN); + assertNotNull(ref); + return ref; + } + + @Nonnull + private static PhysicsProjectionIndexResource projection(@Nonnull Store store) { + return store.getResource(PhysicsProjectionIndexResource.getResourceType()); + } + + private static final class TestPhysicsRef extends Ref { + + private final boolean valid; + + private TestPhysicsRef(int index, boolean valid) { + super(null, index); + this.valid = valid; + } + + @Override + public boolean isValid() { + return valid; + } + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/testsupport/LegacyLiveHandleTestResource.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/testsupport/LegacyLiveHandleTestResource.java deleted file mode 100644 index 471f1cb1..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/internal/testsupport/LegacyLiveHandleTestResource.java +++ /dev/null @@ -1,329 +0,0 @@ -package dev.hytalemodding.impulse.core.internal.testsupport; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsJoint; -import dev.hytalemodding.impulse.api.PhysicsJointType; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.runtime.legacy.LegacyPhysicsBackendRuntime; -import dev.hytalemodding.impulse.core.internal.resources.BackendBodyHandle; -import dev.hytalemodding.impulse.core.internal.resources.BackendJointHandle; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceBinding; -import dev.hytalemodding.impulse.core.internal.resources.PhysicsWorldRuntimeResource; -import dev.hytalemodding.impulse.core.internal.resources.body.PhysicsBodyRegistration; -import dev.hytalemodding.impulse.core.internal.resources.joint.PhysicsJointRegistration; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsMutationHandle; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.JointType; -import java.lang.reflect.Field; -import java.util.List; -import java.util.Map; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3f; - -/** - * Test-only bridge for legacy unit tests that still assert live-object behavior. - */ -public class LegacyLiveHandleTestResource extends PhysicsWorldRuntimeResource { - - private static final Field SPACES = field(LegacyPhysicsBackendRuntime.class, "spaces"); - private static final Field NEXT_BODY_ID = field(LegacyPhysicsBackendRuntime.class, "nextBodyId"); - private static final Field NEXT_JOINT_ID = field(LegacyPhysicsBackendRuntime.class, "nextJointId"); - - @Nonnull - public PhysicsSpace createLiveSpace(@Nonnull BackendId backendId) { - return createLiveSpace(backendId, "test-world", PhysicsSpaceSettings.defaults()); - } - - @Nonnull - public PhysicsSpace createLiveSpace(@Nonnull BackendId backendId, - @Nonnull String worldName) { - return createLiveSpace(backendId, worldName, PhysicsSpaceSettings.defaults()); - } - - @Nonnull - public PhysicsSpace createLiveSpace(@Nonnull BackendId backendId, - @Nonnull String worldName, - @Nonnull PhysicsSpaceSettings settings) { - SpaceId spaceId = createSpace(backendId, worldName, settings); - return liveSpace(spaceId); - } - - @Nonnull - public RigidBodyKey addBody(@Nonnull SpaceId spaceId, - @Nonnull PhysicsBody body, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - return addBody(RigidBodyKey.random(), spaceId, body, kind, persistenceMode); - } - - @Nonnull - public RigidBodyKey addBody(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsBody body, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - return addBodyOnOwner(bodyKey, spaceId, body, kind, persistenceMode); - } - - @Nonnull - public RigidBodyKey addBodyOnOwner(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsBody body, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - RegisteredBody registered = registerLiveBody(spaceId, body); - try { - return super.addBodyOnOwner(bodyKey, spaceId, registered.backendBodyHandle(), kind, persistenceMode); - } catch (RuntimeException exception) { - if (registered.created()) { - unregisterLiveBody(spaceId, registered.backendBodyHandle().value(), body); - } - throw exception; - } - } - - @Nonnull - public PhysicsMutationHandle addBodyAsync(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsBody body, - @Nonnull PhysicsBodyKind kind, - @Nonnull PhysicsBodyPersistenceMode persistenceMode) { - return enqueueOwnerMutation("add test physics body", - bodyKey, - () -> addBodyOnOwner(bodyKey, spaceId, body, kind, persistenceMode)); - } - - @Nullable - public PhysicsBody getBody(@Nonnull RigidBodyKey bodyKey) { - PhysicsBodyRegistration registration = getRegistration(bodyKey); - if (registration == null) { - return null; - } - Object state = legacySpaceState(requireSpaceBinding(registration.spaceId())); - return bodiesById(state).get(registration.backendBodyHandle().value()); - } - - @Nonnull - public PhysicsSpace getLiveSpace(@Nonnull SpaceId spaceId) { - return liveSpace(spaceId); - } - - @Nonnull - public JointKey addJoint(@Nonnull SpaceId spaceId, @Nonnull PhysicsJoint joint) { - BackendJointHandle backendJointHandle = registerLiveJoint(spaceId, joint); - PhysicsBodyRegistration bodyA = getRegistrationByLiveBody(spaceId, joint.getBodyA()); - PhysicsBodyRegistration bodyB = getRegistrationByLiveBody(spaceId, joint.getBodyB()); - if (bodyA == null || bodyB == null) { - throw new IllegalArgumentException("Both joint bodies must be registered before registering the joint"); - } - JointKey jointKey = JointKey.random(); - Vector3f anchorA = joint.getAnchorA(); - Vector3f anchorB = joint.getAnchorB(); - Vector3f axis = joint.getAxis(); - if (axis == null) { - axis = new Vector3f(0.0f, 1.0f, 0.0f); - } - return super.addJointOnOwner(jointKey, - spaceId, - backendJointHandle, - bodyA.bodyKey(), - bodyB.bodyKey(), - jointType(joint.getType()), - anchorA.x, - anchorA.y, - anchorA.z, - anchorB.x, - anchorB.y, - anchorB.z, - axis.x, - axis.y, - axis.z, - 0.0f, - 0.0f, - 0.0f, - joint.getLowerLimit(), - joint.getUpperLimit(), - joint.isMotorEnabled(), - joint.getMotorTargetVelocity(), - joint.getMotorMaxForce()); - } - - @Nullable - public PhysicsJoint getJoint(@Nonnull JointKey jointKey) { - PhysicsJointRegistration registration = getJointRegistration(jointKey); - if (registration == null) { - return null; - } - Object state = legacySpaceState(requireSpaceBinding(registration.spaceId())); - return jointsById(state).get(registration.backendJointHandle().value()); - } - - @Nonnull - private PhysicsSpace liveSpace(@Nonnull SpaceId spaceId) { - Object state = legacySpaceState(requireSpaceBinding(spaceId)); - return liveSpaceFromState(state); - } - - @Nonnull - private RegisteredBody registerLiveBody(@Nonnull SpaceId spaceId, @Nonnull PhysicsBody body) { - Object state = legacySpaceState(requireSpaceBinding(spaceId)); - Map bodyIdsByBody = bodyIdsByBody(state); - Long existing = bodyIdsByBody.get(body); - if (existing != null) { - return new RegisteredBody(new BackendBodyHandle(existing), false); - } - - PhysicsSpace space = liveSpaceFromState(state); - if (!space.getBodies().contains(body)) { - space.addBody(body); - } - - LegacyPhysicsBackendRuntime runtime = legacyRuntime(requireSpaceBinding(spaceId)); - long bodyId = nextId(runtime, NEXT_BODY_ID); - bodiesById(state).put(bodyId, body); - bodyIdsByBody.put(body, bodyId); - return new RegisteredBody(new BackendBodyHandle(bodyId), true); - } - - private void unregisterLiveBody(@Nonnull SpaceId spaceId, - long backendBodyId, - @Nonnull PhysicsBody body) { - Object state = legacySpaceState(requireSpaceBinding(spaceId)); - bodiesById(state).remove(backendBodyId); - bodyIdsByBody(state).remove(body); - liveSpaceFromState(state).removeBody(body); - } - - @Nonnull - private BackendJointHandle registerLiveJoint(@Nonnull SpaceId spaceId, @Nonnull PhysicsJoint joint) { - Object state = legacySpaceState(requireSpaceBinding(spaceId)); - Long existing = liveJointId(state, joint); - if (existing != null) { - return new BackendJointHandle(existing); - } - LegacyPhysicsBackendRuntime runtime = legacyRuntime(requireSpaceBinding(spaceId)); - long jointId = nextId(runtime, NEXT_JOINT_ID); - jointsById(state).put(jointId, joint); - return new BackendJointHandle(jointId); - } - - @Nullable - private PhysicsBodyRegistration getRegistrationByLiveBody(@Nonnull SpaceId spaceId, - @Nonnull PhysicsBody body) { - Long backendBodyId = bodyIdsByBody(legacySpaceState(requireSpaceBinding(spaceId))).get(body); - if (backendBodyId == null) { - return null; - } - RigidBodyKey bodyKey = getBodyKey(spaceId, backendBodyId); - return bodyKey != null ? getRegistration(bodyKey) : null; - } - - @Nonnull - private static JointType jointType(@Nonnull PhysicsJointType type) { - return switch (type) { - case FIXED -> JointType.FIXED; - case POINT -> JointType.POINT; - case HINGE -> JointType.HINGE; - case SLIDER -> JointType.SLIDER; - case SPRING -> JointType.SPRING; - }; - } - - @Nonnull - private static Object legacySpaceState(@Nonnull PhysicsSpaceBinding binding) { - LegacyPhysicsBackendRuntime runtime = legacyRuntime(binding); - Map spaces = spaces(runtime); - Object state = spaces.get(binding.backendSpaceHandle().value()); - if (state == null) { - throw new IllegalStateException("Missing legacy test space state for " + binding.spaceId()); - } - return state; - } - - @Nonnull - private static LegacyPhysicsBackendRuntime legacyRuntime(@Nonnull PhysicsSpaceBinding binding) { - if (binding.runtime() instanceof LegacyPhysicsBackendRuntime runtime) { - return runtime; - } - throw new IllegalStateException("Legacy live-handle test support requires the legacy runtime adapter"); - } - - @Nonnull - @SuppressWarnings("unchecked") - private static Map spaces(@Nonnull LegacyPhysicsBackendRuntime runtime) { - return (Map) get(SPACES, runtime); - } - - @Nonnull - private static PhysicsSpace liveSpaceFromState(@Nonnull Object state) { - return (PhysicsSpace) get(field(state.getClass(), "space"), state); - } - - @Nonnull - @SuppressWarnings("unchecked") - private static Map bodiesById(@Nonnull Object state) { - return (Map) get(field(state.getClass(), "bodiesById"), state); - } - - @Nonnull - @SuppressWarnings("unchecked") - private static Map bodyIdsByBody(@Nonnull Object state) { - return (Map) get(field(state.getClass(), "bodyIdsByBody"), state); - } - - @Nonnull - @SuppressWarnings("unchecked") - private static Map jointsById(@Nonnull Object state) { - return (Map) get(field(state.getClass(), "jointsById"), state); - } - - private static long nextId(@Nonnull LegacyPhysicsBackendRuntime runtime, @Nonnull Field field) { - try { - long id = field.getLong(runtime); - field.setLong(runtime, id + 1L); - return id; - } catch (IllegalAccessException exception) { - throw new IllegalStateException("Cannot update legacy runtime id counter", exception); - } - } - - @Nullable - private static Long liveJointId(@Nonnull Object state, @Nonnull PhysicsJoint joint) { - for (Map.Entry entry : jointsById(state).entrySet()) { - if (entry.getValue() == joint) { - return entry.getKey(); - } - } - return null; - } - - private record RegisteredBody(@Nonnull BackendBodyHandle backendBodyHandle, boolean created) { - } - - @Nonnull - private static Field field(@Nonnull Class owner, @Nonnull String name) { - try { - Field field = owner.getDeclaredField(name); - field.setAccessible(true); - return field; - } catch (ReflectiveOperationException exception) { - throw new IllegalStateException("Cannot access " + owner.getName() + "." + name, exception); - } - } - - @Nonnull - private static Object get(@Nonnull Field field, @Nonnull Object target) { - try { - return field.get(target); - } catch (IllegalAccessException exception) { - throw new IllegalStateException("Cannot read " + field.getName(), exception); - } - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/components/BodyCommandComponentTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/components/BodyCommandComponentTest.java new file mode 100644 index 00000000..036d49d5 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/components/BodyCommandComponentTest.java @@ -0,0 +1,73 @@ +package dev.hytalemodding.impulse.core.plugin.components; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; +import dev.hytalemodding.impulse.core.plugin.components.BodyCommandComponent; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class BodyCommandComponentTest { + + @Test + void appendPreservesOrderAndCopiesEntries() { + BodyCommandComponent first = BodyCommandComponent.wake(); + BodyCommandComponent second = BodyCommandComponent.setVelocity(new Vector3f(1.0f, + 2.0f, + 3.0f), + new Vector3f(0.1f, 0.2f, 0.3f), + true); + + BodyCommandComponent merged = first.append(second); + + BodyCommandComponent.Entry[] entries = merged.entries(); + assertEquals(2, entries.length); + assertEquals(BodyCommandComponent.Kind.WAKE, entries[0].getKind()); + assertEquals(BodyCommandComponent.Kind.SET_VELOCITY, entries[1].getKind()); + assertEquals(1.0f, entries[1].getX(), 0.0001f); + assertEquals(0.3f, entries[1].getAngularZ(), 0.0001f); + assertNotSame(entries[0], merged.entries()[0]); + } + + @Test + void factoriesStoreCommandSpecificFields() { + BodyCommandComponent type = BodyCommandComponent.setType(PhysicsBodyType.KINEMATIC, true); + BodyCommandComponent filter = BodyCommandComponent.setCollisionFilter( + PhysicsCollisionFilters.DYNAMIC_BODY, + PhysicsCollisionFilters.TERRAIN, + false); + BodyCommandComponent impulse = BodyCommandComponent.vector( + BodyCommandComponent.Kind.IMPULSE, + 0.0f, + 6.0f, + 0.0f, + true, + 0.0f, + -0.5f, + 0.0f); + + assertEquals(PhysicsBodyType.KINEMATIC, type.entries()[0].getBodyType()); + assertTrue(type.entries()[0].isActivate()); + assertEquals(PhysicsCollisionFilters.TERRAIN, filter.entries()[0].getCollisionMask()); + assertEquals(BodyCommandComponent.Kind.IMPULSE, impulse.entries()[0].getKind()); + assertTrue(impulse.entries()[0].hasOffset()); + assertEquals(-0.5f, impulse.entries()[0].getOffsetY(), 0.0001f); + } + + @Test + void vectorRejectsNonVectorKind() { + assertThrows(IllegalArgumentException.class, + () -> BodyCommandComponent.vector(BodyCommandComponent.Kind.SET_TYPE, + 0.0f, + 0.0f, + 0.0f, + false, + 0.0f, + 0.0f, + 0.0f)); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyComponentsTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyComponentsTest.java deleted file mode 100644 index 567279e0..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/components/PhysicsBodyComponentsTest.java +++ /dev/null @@ -1,164 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.components; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import org.joml.Quaternionf; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class PhysicsBodyComponentsTest { - - @Test - void splitBodyComponentsCloneValueState() { - RigidBodyKey bodyKey = RigidBodyKey.of(0L, 42L); - PhysicsBodyIdentityComponent identity = new PhysicsBodyIdentityComponent( - bodyKey, - new SpaceId(7), - PhysicsBodyPersistenceMode.PERSISTENT); - PhysicsBodyShapeComponent shape = PhysicsBodyShapeComponent.capsule(0.35f, - 0.8f, - PhysicsAxis.Y); - PhysicsBodyDynamicsComponent dynamics = new PhysicsBodyDynamicsComponent( - PhysicsBodyType.KINEMATIC, - 3.0f, - 0.05f, - 0.1f); - PhysicsBodyMaterialComponent material = new PhysicsBodyMaterialComponent(0.4f, - 0.2f); - PhysicsBodyCollisionComponent collision = new PhysicsBodyCollisionComponent( - false, - PhysicsCollisionFilters.DYNAMIC_BODY, - PhysicsCollisionFilters.TERRAIN | PhysicsCollisionFilters.DYNAMIC_BODY); - PhysicsBodyLifecycleComponent lifecycle = PhysicsBodyLifecycleComponent.pending(bodyKey); - - PhysicsBodyIdentityComponent identityCopy = identity.clone(); - PhysicsBodyShapeComponent shapeCopy = shape.clone(); - PhysicsBodyDynamicsComponent dynamicsCopy = dynamics.clone(); - PhysicsBodyMaterialComponent materialCopy = material.clone(); - PhysicsBodyCollisionComponent collisionCopy = collision.clone(); - PhysicsBodyLifecycleComponent lifecycleCopy = lifecycle.clone(); - - assertNotSame(identity, identityCopy); - assertEquals(bodyKey, identityCopy.getBodyKey()); - assertEquals(new SpaceId(7), identityCopy.getSpaceId()); - assertEquals(PhysicsBodyPersistenceMode.PERSISTENT, identityCopy.getPersistenceMode()); - assertEquals(ShapeType.CAPSULE, shapeCopy.getShapeType()); - assertEquals(0.35f, shapeCopy.getRadius(), 0.0001f); - assertEquals(PhysicsBodyType.KINEMATIC, dynamicsCopy.getBodyType()); - assertEquals(3.0f, dynamicsCopy.getMass(), 0.0001f); - assertEquals(0.4f, materialCopy.getFriction(), 0.0001f); - assertEquals(PhysicsCollisionFilters.DYNAMIC_BODY, collisionCopy.getCollisionGroup()); - assertEquals(PhysicsBodyLifecycleComponent.State.PENDING, lifecycleCopy.getState()); - assertEquals(bodyKey, lifecycleCopy.getBodyKey()); - } - - @Test - void spawnIntentCopiesSplitComponentValuesIntoExistingCommandTypes() { - PhysicsBodyShapeComponent shape = PhysicsBodyShapeComponent.capsule(0.35f, - 0.8f, - PhysicsAxis.Y); - PhysicsBodyDynamicsComponent dynamics = new PhysicsBodyDynamicsComponent( - PhysicsBodyType.DYNAMIC, - 1.0f, - 0.05f, - 0.1f); - PhysicsBodyMaterialComponent material = new PhysicsBodyMaterialComponent(0.4f, - 0.2f); - PhysicsBodyCollisionComponent collision = new PhysicsBodyCollisionComponent( - false, - PhysicsCollisionFilters.DYNAMIC_BODY, - PhysicsCollisionFilters.TERRAIN | PhysicsCollisionFilters.DYNAMIC_BODY); - - PhysicsShapeSpec shapeSpec = PhysicsBodyComponentValues.toShapeSpec(shape); - RigidBodySpawnSettings settings = PhysicsBodyComponentValues.toSpawnSettings( - dynamics, - material, - collision); - - assertEquals(ShapeType.CAPSULE, shapeSpec.type()); - assertEquals(0.35f, shapeSpec.radius(), 0.0001f); - assertEquals(0.8f, shapeSpec.halfHeight(), 0.0001f); - assertEquals(PhysicsAxis.Y, shapeSpec.axis()); - assertTrue(settings.hasFriction()); - assertEquals(0.4f, settings.friction(), 0.0001f); - assertTrue(settings.hasLinearDamping()); - assertEquals(0.05f, settings.linearDamping(), 0.0001f); - assertTrue(settings.hasCollisionFilter()); - assertEquals(PhysicsCollisionFilters.DYNAMIC_BODY, settings.collisionGroup()); - assertFalse(settings.sensor()); - } - - @Test - void defaultIntentValuesStayFriendlyButExplicitSpaceIsRequired() { - PhysicsBodyIdentityComponent identity = new PhysicsBodyIdentityComponent(); - PhysicsBodyShapeComponent shape = new PhysicsBodyShapeComponent(); - PhysicsBodyDynamicsComponent dynamics = new PhysicsBodyDynamicsComponent(); - PhysicsBodyMaterialComponent material = new PhysicsBodyMaterialComponent(); - PhysicsBodyCollisionComponent collision = new PhysicsBodyCollisionComponent(); - - assertFalse(PhysicsBodyComponentValues.hasExplicitSpace(identity)); - assertEquals(ShapeType.BOX, shape.getShapeType()); - assertEquals(0.5f, shape.getHalfExtentX(), 0.0001f); - assertEquals(PhysicsBodyType.DYNAMIC, dynamics.getBodyType()); - assertEquals(1.0f, dynamics.getMass(), 0.0001f); - assertEquals(0.5f, material.getFriction(), 0.0001f); - assertEquals(PhysicsCollisionFilters.DYNAMIC_BODY, collision.getCollisionGroup()); - assertEquals(PhysicsCollisionFilters.ALL, collision.getCollisionMask()); - assertEquals(PhysicsBodyPersistenceMode.RUNTIME_ONLY, identity.getPersistenceMode()); - } - - @Test - void kinematicTargetCopiesValueState() { - PhysicsBodyKinematicTargetComponent target = new PhysicsBodyKinematicTargetComponent( - new Vector3f(1.0f, 2.0f, 3.0f), - new Quaternionf(), - new Vector3f(0.25f, 0.0f, 0.0f), - new Vector3f(0.0f, 0.5f, 0.0f), - true, - true, - false); - - PhysicsBodyKinematicTargetComponent copy = target.clone(); - - assertNotSame(target, copy); - assertEquals(1.0f, copy.getPosition().x, 0.0001f); - assertEquals(0.25f, copy.getLinearVelocity().x, 0.0001f); - assertTrue(copy.isVelocityEnabled()); - assertFalse(copy.isActivate()); - } - - @Test - void nonNullPublicComponentFieldsRejectNulls() { - assertThrows(NullPointerException.class, () -> new PhysicsBodyIdentityComponent( - null, - new SpaceId(1), - PhysicsBodyPersistenceMode.PERSISTENT)); - assertThrows(NullPointerException.class, - () -> PhysicsBodyShapeComponent.capsule(0.25f, 0.5f, null)); - assertThrows(NullPointerException.class, - () -> new PhysicsBodyDynamicsComponent(null, 1.0f, 0.0f, 0.0f)); - assertThrows(NullPointerException.class, () -> new PhysicsBodyKinematicTargetComponent( - null, - new Quaternionf(), - new Vector3f(), - new Vector3f(), - true, - false, - true)); - assertThrows(NullPointerException.class, - () -> new PhysicsBodyLifecycleComponent(null, null, null)); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/events/DeprecatedPhysicsEventPluginApiTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/events/DeprecatedPhysicsEventPluginApiTest.java new file mode 100644 index 00000000..27b39d49 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/events/DeprecatedPhysicsEventPluginApiTest.java @@ -0,0 +1,51 @@ +package dev.hytalemodding.impulse.core.plugin.events; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsEntityTypes; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsWorlds; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; +import java.lang.reflect.AnnotatedElement; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("deprecation") +class DeprecatedPhysicsEventPluginApiTest { + + @Test + void marksPhysicsEventValueApiDeprecated() throws NoSuchFieldException { + assertDeprecated(PhysicsEventFrame.class.getPackage()); + assertDeprecated(PhysicsBodyActivationEvent.class); + assertDeprecated(PhysicsContactEvent.class); + assertDeprecated(PhysicsEventCollectionMode.class); + assertDeprecated(PhysicsEventCollectionMode.DISABLED.getClass().getField("DISABLED")); + assertDeprecated(PhysicsEventCollectionMode.CONTACTS.getClass().getField("CONTACTS")); + assertDeprecated(PhysicsEventFrame.class); + assertDeprecated(PhysicsEventFramePublishedEvent.class); + assertDeprecated(PhysicsFrameEvent.class); + assertDeprecated(PhysicsFrameEventKind.class); + assertDeprecated(PhysicsFrameEventKind.CONTACT.getClass().getField("CONTACT")); + assertDeprecated(PhysicsFrameEventKind.BODY_ACTIVATION.getClass().getField("BODY_ACTIVATION")); + assertDeprecated(PhysicsFrameEventKind.JOINT_BREAK.getClass().getField("JOINT_BREAK")); + assertDeprecated(PhysicsJointBreakEvent.class); + assertDeprecated(PhysicsSnapshotPublicationEvent.class); + assertDeprecated(PhysicsStepEvent.class); + } + + @Test + void marksPhysicsEventAccessorsDeprecated() throws NoSuchMethodException, NoSuchFieldException { + assertDeprecated(PhysicsWorlds.class.getMethod("latestEventFrame", Store.class)); + assertDeprecated(PhysicsWorlds.class.getMethod("latestEventFrameAsync", World.class)); + assertDeprecated(PhysicsWorldSettings.class.getField("DEFAULT_EVENT_COLLECTION_MODE")); + assertDeprecated(PhysicsWorldSettings.class.getMethod("getEventCollectionMode")); + assertDeprecated(PhysicsWorldSettings.class.getMethod("setEventCollectionMode", + PhysicsEventCollectionMode.class)); + assertDeprecated(PhysicsEntityTypes.class.getMethod("physicsEventFramePublishedEventType")); + } + + private static void assertDeprecated(AnnotatedElement element) { + assertNotNull(element.getAnnotation(Deprecated.class), () -> element + " is not deprecated"); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/components/BodyAttachmentComponentTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/components/BodyAttachmentComponentTest.java new file mode 100644 index 00000000..5c25f833 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/modules/physicsentity/components/BodyAttachmentComponentTest.java @@ -0,0 +1,72 @@ +package dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.UUID; + +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class BodyAttachmentComponentTest { + + @Test + void attachmentClonesUuidAndProjectionState() { + UUID bodyUuid = UUID.fromString("00000000-0000-0000-0000-000000000042"); + BodyAttachmentComponent attachment = BodyAttachmentComponent.impulseOwnedVisual( + bodyUuid, + new Vector3f(1.0f, 2.0f, 3.0f), + new Quaternionf().rotateY(0.5f), + 0.25f); + + BodyAttachmentComponent copy = attachment.clone(); + + assertNotSame(attachment, copy); + assertEquals(bodyUuid, copy.getBodyUuid()); + assertEquals(BodyAttachmentComponent.TransformAuthority.BODY, + copy.getTransformAuthority()); + assertEquals(BodyAttachmentComponent.AttachmentLifecycle.IMPULSE_OWNED_VISUAL, + copy.getLifecycle()); + assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), copy.getLocalPositionOffset()); + assertEquals(0.25f, copy.getVisualOriginOffsetY(), 0.0001f); + assertTrue(copy.shouldRemoveEntityWhenBodyMissing()); + } + + @Test + void externalEntityDefaultsToBodyAuthorityAndKeepsEntityWhenMissing() { + UUID bodyUuid = UUID.randomUUID(); + + BodyAttachmentComponent attachment = BodyAttachmentComponent.externalEntity(bodyUuid); + + assertEquals(bodyUuid, attachment.getBodyUuid()); + assertEquals(BodyAttachmentComponent.TransformAuthority.BODY, + attachment.getTransformAuthority()); + assertEquals(BodyAttachmentComponent.AttachmentLifecycle.EXTERNAL_ENTITY, + attachment.getLifecycle()); + assertFalse(attachment.shouldRemoveEntityWhenBodyMissing()); + } + + @Test + void normalizesInvalidVisualOriginOffsetToBodyValue() { + BodyAttachmentComponent attachment = new BodyAttachmentComponent( + UUID.randomUUID(), + BodyAttachmentComponent.TransformAuthority.BODY, + BodyAttachmentComponent.AttachmentLifecycle.GENERATED_PROXY, + new Vector3f(), + new Quaternionf(), + Float.NaN); + + assertEquals(-1.0f, attachment.getVisualOriginOffsetY(), 0.0001f); + assertEquals(0.75f, attachment.resolveVisualOriginOffsetY(0.75f), 0.0001f); + } + + @Test + void rejectsMissingBodyUuid() { + assertThrows(NullPointerException.class, + () -> BodyAttachmentComponent.externalEntity(null)); + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsBodiesTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsBodiesTest.java new file mode 100644 index 00000000..65f57388 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsBodiesTest.java @@ -0,0 +1,141 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.thread.TickingThread; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSnapshotResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsSpaceCompatibilityIndexResource; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsSnapshotFrame; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Quaternionf; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class PhysicsBodiesTest { + + @Test + void registrationViewsAreDerivedFromSnapshotsWithCompatibleSpaces() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("physics-bodies-snapshot-index")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID firstSpaceUuid = uuid(1); + UUID secondSpaceUuid = uuid(2); + UUID unmappedSpaceUuid = uuid(3); + SpaceId firstSpaceId = new SpaceId(41); + SpaceId secondSpaceId = new SpaceId(42); + Ref firstBodyRef = new TestRef(store, 11); + Ref secondBodyRef = new TestRef(store, 12); + Ref unmappedBodyRef = new TestRef(store, 13); + UUID firstBodyUuid = uuid(101); + UUID secondBodyUuid = uuid(102); + UUID unmappedBodyUuid = uuid(103); + + store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .putSpace(firstSpaceId, firstSpaceUuid); + store.getResource(PhysicsSpaceCompatibilityIndexResource.getResourceType()) + .putSpace(secondSpaceId, secondSpaceUuid); + store.getResource(PhysicsSnapshotResource.getResourceType()) + .publish(new PhysicsSnapshotFrame(1L, + 0.05f, + List.of(snapshot(firstBodyRef, firstBodyUuid, firstSpaceUuid), + snapshot(secondBodyRef, secondBodyUuid, secondSpaceUuid), + snapshot(unmappedBodyRef, unmappedBodyUuid, unmappedSpaceUuid)))); + + assertEquals(firstSpaceId, PhysicsBodies.spaceId(store, firstBodyUuid)); + assertEquals(secondSpaceId, PhysicsBodies.spaceId(store, secondBodyRef)); + assertNull(PhysicsBodies.spaceId(store, unmappedBodyUuid)); + assertTrue(PhysicsBodies.isRegistered(store, firstBodyUuid)); + assertTrue(PhysicsBodies.isRegistered(store, secondBodyRef)); + assertFalse(PhysicsBodies.isRegistered(store, unmappedBodyUuid)); + assertEquals(List.of(firstBodyUuid, secondBodyUuid), + List.copyOf(PhysicsBodies.bodyUuids(store))); + assertEquals(2, PhysicsBodies.registrationCount(store)); + assertEquals(1, PhysicsBodies.registrationCount(store, firstSpaceId)); + assertEquals(1, PhysicsBodies.registrationCount(store, secondSpaceId)); + assertEquals(0, PhysicsBodies.registrationCount(store, new SpaceId(404))); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void bodyRegistrationResourceTypeIsNotExposed() { + assertThrows(NoSuchMethodException.class, + () -> PhysicsResourceTypes.class.getDeclaredMethod("bodyRegistrationResourceType")); + } + + @Nonnull + private static UUID uuid(long lowBits) { + return new UUID(0L, lowBits); + } + + @Nonnull + private static PhysicsBodySnapshot snapshot(@Nonnull Ref bodyRef, + @Nonnull UUID bodyUuid, + @Nonnull UUID spaceUuid) { + return new PhysicsBodySnapshot(bodyRef, + bodyUuid, + spaceUuid, + PhysicsBodyType.DYNAMIC, + new Vector3f(), + new Quaternionf(), + new Vector3f(), + new Vector3f(), + 0.0f, + false); + } + + private static void markCurrentThreadAsWorldThread(@Nonnull Store store) { + try { + Method setThread = TickingThread.class.getDeclaredMethod("setThread", Thread.class); + setThread.setAccessible(true); + setThread.invoke(store.getExternalData().getWorld(), Thread.currentThread()); + } catch (NoSuchMethodException | IllegalAccessException exception) { + throw new AssertionError("Could not mark test world thread", exception); + } catch (InvocationTargetException exception) { + throw new AssertionError("Could not mark test world thread", + exception.getTargetException()); + } + } + + private static final class TestRef extends Ref { + + private TestRef(@Nonnull Store store, int index) { + super(store, index); + } + + @Override + public boolean isValid() { + return true; + } + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsBodyEntitiesTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsBodyEntitiesTest.java new file mode 100644 index 00000000..23639122 --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsBodyEntitiesTest.java @@ -0,0 +1,114 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.thread.TickingThread; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.components.BodyComponent; +import dev.hytalemodding.impulse.core.plugin.components.ColliderComponent; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.MaterialComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class PhysicsBodyEntitiesTest { + + @Test + void physicsBodiesDoesNotExposeBodyUuidRefHelper() { + assertThrows(NoSuchMethodException.class, + () -> PhysicsBodies.class.getDeclaredMethod("bodyUuid", Store.class, Ref.class)); + } + + @Test + void dynamicBodyHolderInfersEntityIdentityFromBodyUuidAndSpaceRef() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("physics-body-entities-holder-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + UUID spaceUuid = uuid(1); + UUID bodyUuid = uuid(2); + Ref spaceRef = PhysicsSpaces.create(store, + spaceUuid, + new SpaceId(4101), + new BackendId("test:body-entities")); + + Holder holder = PhysicsBodyEntities.dynamicBodyHolder(spaceRef, + bodyUuid, + new Vector3f(1.0f, 2.0f, 3.0f), + PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f), + 1.0f, + RigidBodySpawnSettings.defaults(), + null); + Ref bodyRef = store.addEntity(holder, AddReason.SPAWN); + + assertEquals(bodyUuid, + store.getComponent(bodyRef, UuidComponent.getComponentType()).getUuid()); + BodyComponent body = store.getComponent(bodyRef, BodyComponent.getComponentType()); + assertEquals(spaceUuid, body.getSpaceUuid()); + assertSame(spaceRef, body.getSpaceRef()); + assertEquals(1.0f, + store.getComponent(bodyRef, DynamicsComponent.getComponentType()).getMass()); + assertEquals(0.5f, + store.getComponent(bodyRef, ShapeComponent.getComponentType()).getHalfExtentX()); + assertEquals(0.5f, + store.getComponent(bodyRef, MaterialComponent.getComponentType()).getFriction()); + assertEquals(0.0f, + store.getComponent(bodyRef, ColliderComponent.getComponentType()) + .getLocalPosition() + .x); + assertEquals(PhysicsCollisionFilters.DYNAMIC_BODY, + store.getComponent(bodyRef, CollisionFilterComponent.getComponentType()) + .getCollisionGroup()); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Nonnull + private static UUID uuid(int lowBits) { + return new UUID(0L, lowBits); + } + + private static void markCurrentThreadAsWorldThread(@Nonnull Store store) { + try { + Method setThread = TickingThread.class.getDeclaredMethod("setThread", Thread.class); + setThread.setAccessible(true); + setThread.invoke(store.getExternalData().getWorld(), Thread.currentThread()); + } catch (NoSuchMethodException | IllegalAccessException exception) { + throw new AssertionError("Could not mark test world thread", exception); + } catch (InvocationTargetException exception) { + throw new AssertionError("Could not mark test world thread", + exception.getTargetException()); + } + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsShapeSpecTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsShapeSpecTest.java similarity index 94% rename from impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsShapeSpecTest.java rename to impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsShapeSpecTest.java index fcdcd9f2..54e16e82 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsShapeSpecTest.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsShapeSpecTest.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; +package dev.hytalemodding.impulse.core.plugin.physics; import static org.junit.jupiter.api.Assertions.assertEquals; diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsSpacesSettingsComponentTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsSpacesSettingsComponentTest.java new file mode 100644 index 00000000..40e3ec4b --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/PhysicsSpacesSettingsComponentTest.java @@ -0,0 +1,372 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.thread.TickingThread; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.components.ExtensionSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.components.SolverSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.ChunkCollisionSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.components.CollisionLodSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsChunkCollisionSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsCollisionLodSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.VisualMaterializationSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.VisualSyncSettingsComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualMaterializationSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualSyncSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.VisualOcclusionMode; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsBackendExtensionId; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsExtensionSettings; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSolverSettings; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.UUID; +import javax.annotation.Nonnull; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class PhysicsSpacesSettingsComponentTest { + + private static final PhysicsBackendExtensionId RAPIER_SOLVER_EXTENSION_ID = + new PhysicsBackendExtensionId("impulse:rapier_solver"); + private static final String RAPIER_INTERNAL_PGS_ITERATIONS = "internalPgsIterations"; + private static final String RAPIER_MIN_ISLAND_SIZE = "minIslandSize"; + + @Test + void defaultSpaceSettingsHolderDoesNotMaterializeDefaultComponents() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("physics-space-default-holder-test")), + EmptyResourceStorage.get()); + try { + Holder holder = PhysicsEntities.spaceHolder(store, + uuid(1), + new SpaceComponent(new BackendId("test:settings-holder"), + new Vector3f(0.0f, -9.81f, 0.0f)), + new ChunkCollisionSettingsComponent(), + new SolverSettingsComponent(), + new VisualSyncSettingsComponent(), + new VisualMaterializationSettingsComponent(), + new CollisionLodSettingsComponent(), + new ExtensionSettingsComponent()); + Ref ref = store.addEntity(holder, AddReason.SPAWN); + assertNotNull(ref); + + assertNull(store.getComponent(ref, ChunkCollisionSettingsComponent.getComponentType())); + assertNull(store.getComponent(ref, SolverSettingsComponent.getComponentType())); + assertNull(store.getComponent(ref, VisualSyncSettingsComponent.getComponentType())); + assertNull(store.getComponent(ref, + VisualMaterializationSettingsComponent.getComponentType())); + assertNull(store.getComponent(ref, CollisionLodSettingsComponent.getComponentType())); + assertNull(store.getComponent(ref, ExtensionSettingsComponent.getComponentType())); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void domainSettingWritesAddAndRemoveOnlyTheirOwnComponents() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("physics-space-domain-settings-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + Ref spaceRef = PhysicsSpaces.create(store, + uuid(2), + new SpaceId(2001), + new BackendId("test:settings-domain")); + + PhysicsSolverSettings solverSettings = PhysicsSpaces.solverSettings(store, spaceRef); + assertNotNull(solverSettings); + assertEquals(PhysicsSolverSettings.DEFAULT_SOLVER_ITERATIONS, + solverSettings.getSolverIterations()); + assertNull(store.getComponent(spaceRef, SolverSettingsComponent.getComponentType())); + + solverSettings.setSolverIterations(6); + PhysicsSpaces.putSolverSettings(store, spaceRef, solverSettings); + assertNotNull(store.getComponent(spaceRef, + SolverSettingsComponent.getComponentType())); + assertNull(store.getComponent(spaceRef, + CollisionLodSettingsComponent.getComponentType())); + + PhysicsCollisionLodSettings collisionLodSettings = + PhysicsSpaces.collisionLodSettings(store, spaceRef); + assertNotNull(collisionLodSettings); + collisionLodSettings.setCollisionLodEnabled(true); + PhysicsSpaces.putCollisionLodSettings(store, spaceRef, collisionLodSettings); + assertNotNull(store.getComponent(spaceRef, + CollisionLodSettingsComponent.getComponentType())); + assertNotNull(store.getComponent(spaceRef, + SolverSettingsComponent.getComponentType())); + + PhysicsVisualSyncSettings visualSyncSettings = + PhysicsSpaces.visualSyncSettings(store, spaceRef); + assertNotNull(visualSyncSettings); + assertNull(store.getComponent(spaceRef, + VisualSyncSettingsComponent.getComponentType())); + visualSyncSettings.setVisualMidSyncIntervalTicks(2); + PhysicsSpaces.putVisualSyncSettings(store, spaceRef, visualSyncSettings); + assertNotNull(store.getComponent(spaceRef, + VisualSyncSettingsComponent.getComponentType())); + assertNotNull(store.getComponent(spaceRef, + CollisionLodSettingsComponent.getComponentType())); + + PhysicsVisualMaterializationSettings visualMaterializationSettings = + PhysicsSpaces.visualMaterializationSettings(store, spaceRef); + assertNotNull(visualMaterializationSettings); + assertNull(store.getComponent(spaceRef, + VisualMaterializationSettingsComponent.getComponentType())); + visualMaterializationSettings.setDetachedVisualMaterializationEnabled(true); + PhysicsSpaces.putVisualMaterializationSettings(store, + spaceRef, + visualMaterializationSettings); + assertNotNull(store.getComponent(spaceRef, + VisualMaterializationSettingsComponent.getComponentType())); + assertNotNull(store.getComponent(spaceRef, + VisualSyncSettingsComponent.getComponentType())); + + solverSettings.setSolverIterations(PhysicsSolverSettings.DEFAULT_SOLVER_ITERATIONS); + PhysicsSpaces.putSolverSettings(store, spaceRef, solverSettings); + assertNull(store.getComponent(spaceRef, SolverSettingsComponent.getComponentType())); + assertNotNull(store.getComponent(spaceRef, + CollisionLodSettingsComponent.getComponentType())); + + visualSyncSettings.setVisualMidSyncIntervalTicks( + PhysicsVisualSyncSettings.DEFAULT_VISUAL_MID_SYNC_INTERVAL_TICKS); + PhysicsSpaces.putVisualSyncSettings(store, spaceRef, visualSyncSettings); + assertNull(store.getComponent(spaceRef, + VisualSyncSettingsComponent.getComponentType())); + assertNotNull(store.getComponent(spaceRef, + VisualMaterializationSettingsComponent.getComponentType())); + + visualMaterializationSettings.setDetachedVisualMaterializationEnabled( + PhysicsVisualMaterializationSettings + .DEFAULT_DETACHED_VISUAL_MATERIALIZATION_ENABLED); + PhysicsSpaces.putVisualMaterializationSettings(store, + spaceRef, + visualMaterializationSettings); + assertNull(store.getComponent(spaceRef, + VisualMaterializationSettingsComponent.getComponentType())); + assertNotNull(store.getComponent(spaceRef, + CollisionLodSettingsComponent.getComponentType())); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void explicitSpaceLifecycleCleansCompatibilityIndexAndSettings() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("physics-space-lifecycle-round-trip")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + int previousCount = PhysicsSpaces.count(store); + SpaceId spaceId = new SpaceId(2002); + + PhysicsSpaces.create(store, + uuid(3), + spaceId, + new BackendId("test:settings-lifecycle")); + PhysicsSpaces.putChunkCollisionSettings(store, + spaceId, + populatedChunkCollisionSettings()); + + PhysicsChunkCollisionSettings settings = + PhysicsSpaces.chunkCollisionSettings(store, spaceId); + assertNotNull(settings); + assertEquals(PhysicsChunkCollisionMode.STREAMING, settings.getMode()); + assertTrue(PhysicsSpaces.hasSpace(store, spaceId)); + assertNotNull(PhysicsSpaces.resolveRef(store, spaceId)); + + PhysicsSpaces.removeEmpty(store, spaceId); + + assertEquals(previousCount, PhysicsSpaces.count(store)); + assertFalse(PhysicsSpaces.hasSpace(store, spaceId)); + assertNull(PhysicsSpaces.resolveRef(store, spaceId)); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void populatedSpaceSettingsRoundTripThroughPublicFacade() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("physics-space-settings-round-trip")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + SpaceId spaceId = new SpaceId(2003); + Ref spaceRef = PhysicsSpaces.create(store, + uuid(4), + spaceId, + new BackendId("test:settings-round-trip")); + + PhysicsSpaces.putChunkCollisionSettings(store, + spaceId, + populatedChunkCollisionSettings()); + PhysicsSpaces.putVisualSyncSettings(store, spaceId, populatedVisualSyncSettings()); + PhysicsSpaces.putSolverSettings(store, spaceId, populatedSolverSettings()); + PhysicsSpaces.putExtensionSettings(store, spaceId, populatedExtensionSettings()); + PhysicsSpaces.putVisualMaterializationSettings(store, + spaceId, + populatedVisualMaterializationSettings()); + + PhysicsChunkCollisionSettings chunkCollision = + PhysicsSpaces.chunkCollisionSettings(store, spaceRef); + PhysicsVisualSyncSettings visualSync = PhysicsSpaces.visualSyncSettings(store, spaceRef); + PhysicsSolverSettings solver = PhysicsSpaces.solverSettings(store, spaceRef); + PhysicsExtensionSettings extension = PhysicsSpaces.extensionSettings(store, spaceRef); + PhysicsVisualMaterializationSettings visualMaterialization = + PhysicsSpaces.visualMaterializationSettings(store, spaceRef); + assertNotNull(chunkCollision); + assertNotNull(visualSync); + assertNotNull(solver); + assertNotNull(extension); + assertNotNull(visualMaterialization); + assertEquals(PhysicsChunkCollisionMode.STREAMING, chunkCollision.getMode()); + assertEquals(9, chunkCollision.getRadius()); + assertEquals(5, chunkCollision.getBodyRadius()); + assertEquals(77, chunkCollision.getTtlTicks()); + assertEquals(48, visualSync.getVisualFullSyncRadius()); + assertEquals(96, visualSync.getVisualMaxSyncRadius()); + assertFalse(visualSync.isVisualFarSyncCutoffEnabled()); + assertEquals(3, visualSync.getVisualMidSyncIntervalTicks()); + assertEquals(17, visualSync.getVisualFarSyncIntervalTicks()); + assertEquals(VisualOcclusionMode.PRIORITY, visualSync.getVisualOcclusionMode()); + assertEquals(31, visualSync.getVisualOcclusionRaycastsPerTick()); + assertEquals(7, visualSync.getVisualOcclusionCacheTicks()); + assertTrue(visualSync.isEntityVisualSyncCullingEnabled()); + assertTrue(visualSync.isVisualVisibilityCullingEnabled()); + assertEquals(5, solver.getSolverIterations()); + assertEquals(1, solver.getStabilizationIterations()); + assertEquals(2, extension.getInt(RAPIER_SOLVER_EXTENSION_ID, + RAPIER_INTERNAL_PGS_ITERATIONS).orElseThrow()); + assertEquals(64, extension.getInt(RAPIER_SOLVER_EXTENSION_ID, + RAPIER_MIN_ISLAND_SIZE).orElseThrow()); + assertTrue(visualMaterialization.isDetachedVisualMaterializationEnabled()); + assertEquals(48, visualMaterialization.getDetachedVisualMaterializationRadius()); + assertEquals(72, visualMaterialization.getDetachedVisualDematerializationRadius()); + assertEquals(33, visualMaterialization.getDetachedVisualMaxSpawnsPerTick()); + assertEquals(444, visualMaterialization.getDetachedVisualMaxMaterialized()); + assertEquals("Rock_Stone", visualMaterialization.getDetachedVisualBlockType()); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Nonnull + private static UUID uuid(int lowBits) { + return new UUID(0L, lowBits); + } + + private static void markCurrentThreadAsWorldThread(@Nonnull Store store) { + try { + Method setThread = TickingThread.class.getDeclaredMethod("setThread", Thread.class); + setThread.setAccessible(true); + setThread.invoke(store.getExternalData().getWorld(), Thread.currentThread()); + } catch (NoSuchMethodException | IllegalAccessException exception) { + throw new AssertionError("Could not mark test world thread", exception); + } catch (InvocationTargetException exception) { + throw new AssertionError("Could not mark test world thread", + exception.getTargetException()); + } + } + + @Nonnull + private static PhysicsChunkCollisionSettings populatedChunkCollisionSettings() { + PhysicsChunkCollisionSettings settings = new PhysicsChunkCollisionSettings(); + settings.setMode(PhysicsChunkCollisionMode.STREAMING); + settings.setRadius(9); + settings.setBodyRadius(5); + settings.setTtlTicks(77); + return settings; + } + + @Nonnull + private static PhysicsVisualSyncSettings populatedVisualSyncSettings() { + PhysicsVisualSyncSettings settings = new PhysicsVisualSyncSettings(); + settings.setVisualMaxSyncRadius(96); + settings.setVisualFullSyncRadius(48); + settings.setVisualFarSyncCutoffEnabled(false); + settings.setVisualMidSyncIntervalTicks(3); + settings.setVisualFarSyncIntervalTicks(17); + settings.setVisualOcclusionMode(VisualOcclusionMode.PRIORITY); + settings.setVisualOcclusionRaycastsPerTick(31); + settings.setVisualOcclusionCacheTicks(7); + settings.setEntityVisualSyncCullingEnabled(true); + settings.setVisualVisibilityCullingEnabled(true); + return settings; + } + + @Nonnull + private static PhysicsSolverSettings populatedSolverSettings() { + PhysicsSolverSettings settings = new PhysicsSolverSettings(); + settings.setSolverIterations(5); + settings.setStabilizationIterations(1); + return settings; + } + + @Nonnull + private static PhysicsExtensionSettings populatedExtensionSettings() { + PhysicsExtensionSettings settings = new PhysicsExtensionSettings(); + settings.setInt(RAPIER_SOLVER_EXTENSION_ID, RAPIER_INTERNAL_PGS_ITERATIONS, 2); + settings.setInt(RAPIER_SOLVER_EXTENSION_ID, RAPIER_MIN_ISLAND_SIZE, 64); + return settings; + } + + @Nonnull + private static PhysicsVisualMaterializationSettings populatedVisualMaterializationSettings() { + PhysicsVisualMaterializationSettings settings = + new PhysicsVisualMaterializationSettings(); + settings.setDetachedVisualMaterializationEnabled(true); + settings.setDetachedVisualDematerializationRadius(72); + settings.setDetachedVisualMaterializationRadius(48); + settings.setDetachedVisualMaxSpawnsPerTick(33); + settings.setDetachedVisualMaxMaterialized(444); + settings.setDetachedVisualBlockType("Rock_Stone"); + return settings; + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/RaycastHitViewTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/RaycastHitViewTest.java new file mode 100644 index 00000000..7802d7cf --- /dev/null +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/RaycastHitViewTest.java @@ -0,0 +1,67 @@ +package dev.hytalemodding.impulse.core.plugin.physics; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.PhysicsAxis; +import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.ShapeType; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.testsupport.TestInstanceFactory; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.ShapeComponent; +import java.util.ArrayList; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; + +class RaycastHitViewTest { + + @Test + void infersBodyAndShapeTypesFromBodyRefComponents() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(TestInstanceFactory.world("raycast-hit-view-type-inference-test")), + EmptyResourceStorage.get()); + try { + Holder holder = registry.newHolder(); + holder.putComponent(DynamicsComponent.getComponentType(), + new DynamicsComponent(PhysicsBodyType.KINEMATIC, + 0.0f, + 0.0f, + 0.0f, + false)); + holder.putComponent(ShapeComponent.getComponentType(), + new ShapeComponent(ShapeType.CAPSULE, + 0.5f, + 0.5f, + 0.5f, + 0.25f, + 1.0f, + PhysicsAxis.Y, + 0.0f, + "")); + Ref bodyRef = store.addEntity(holder, AddReason.SPAWN); + RaycastHitView view = new RaycastHitView(bodyRef, + new Vector3f(1.0f, 2.0f, 3.0f), + new Vector3f(0.0f, 1.0f, 0.0f), + 0.25f, + 4.5f); + + assertEquals(PhysicsBodyType.KINEMATIC, view.bodyType()); + assertEquals(ShapeType.CAPSULE, view.shapeType()); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } +} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/simulation/RigidBodySpawnSettingsTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/RigidBodySpawnSettingsTest.java similarity index 95% rename from impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/simulation/RigidBodySpawnSettingsTest.java rename to impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/RigidBodySpawnSettingsTest.java index 79d7b8b2..30b059f9 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/simulation/RigidBodySpawnSettingsTest.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/physics/RigidBodySpawnSettingsTest.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; +package dev.hytalemodding.impulse.core.plugin.physics; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandContextTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandContextTest.java deleted file mode 100644 index 8c39ad3c..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/simulation/PhysicsCommandContextTest.java +++ /dev/null @@ -1,375 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.internal.simulation.recorder.MutablePhysicsCommandContext; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import java.util.concurrent.atomic.AtomicReference; -import java.util.UUID; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.JointCommandRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.RigidBodyCommandRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.RigidBodySpawnBatchRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.RigidBodySpawnRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.RigidBodySpawnTemplateRecorder; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class PhysicsCommandContextTest { - - @Test - void freezeCapturesMetadataCountAndRejectsFurtherAppends() { - RigidBodyKey bodyKey = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000001")); - Vector3f linearVelocity = new Vector3f(1.0f, 2.0f, 3.0f); - Vector3f angularVelocity = new Vector3f(4.0f, 5.0f, 6.0f); - MutablePhysicsCommandContext buffer = new MutablePhysicsCommandContext(42L, 7L, 4); - - buffer.body(bodyKey) - .setVelocity(linearVelocity.x, linearVelocity.y, linearVelocity.z, - angularVelocity.x, angularVelocity.y, angularVelocity.z, true); - PhysicsCommandBatch batch = buffer.freezeInternal(11L).publicBatch(); - linearVelocity.set(9.0f, 9.0f, 9.0f); - angularVelocity.set(8.0f, 8.0f, 8.0f); - - assertThrows(IllegalStateException.class, - () -> buffer.body(bodyKey).destroy()); - assertEquals(42L, batch.metadata().submittedServerTick()); - assertEquals(11L, batch.metadata().commandBatchSequence()); - assertEquals(1, batch.commandCount()); - } - - @Test - void fluentDslRecordsComposableRecorderOperations() { - RigidBodyKey bodyKey = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000002")); - Vector3f linearVelocity = new Vector3f(1.0f, 0.0f, 0.0f); - Vector3f angularVelocity = new Vector3f(0.0f, 1.0f, 0.0f); - PhysicsCommandRecipe wakeAndMove = commands -> commands.body(bodyKey, - body -> body.setVelocity(linearVelocity, angularVelocity) - .activate()); - MutablePhysicsCommandContext buffer = new MutablePhysicsCommandContext(43L, 8L); - - buffer.compose(wakeAndMove); - linearVelocity.set(99.0f, 99.0f, 99.0f); - angularVelocity.set(88.0f, 88.0f, 88.0f); - - PhysicsCommandBatch batch = buffer.freezeInternal(12L).publicBatch(); - - assertEquals(2, batch.commandCount()); - } - - @Test - void thinBodyOperationsRecordThroughRecipesWithoutBodyRecorder() { - RigidBodyKey bodyKey = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000012")); - PhysicsCommandRecipe wakeAndMove = commands -> commands - .setBodyVelocity(bodyKey, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, true) - .applyBodyImpulse(bodyKey, 7.0f, 8.0f, 9.0f) - .applyBodyForce(bodyKey, 1.5f, 2.5f, 3.5f, 0.5f, 0.6f, 0.7f) - .setBodyType(bodyKey, PhysicsBodyType.KINEMATIC, true) - .activateBody(bodyKey) - .destroyBody(bodyKey); - MutablePhysicsCommandContext buffer = new MutablePhysicsCommandContext(43L, 8L, 6); - - buffer.compose(wakeAndMove); - - PhysicsCommandBatch batch = buffer.freezeInternal(12L).publicBatch(); - - assertEquals(6, batch.commandCount()); - } - - @Test - void bodyRecipeScopesRecorderToCallback() { - RigidBodyKey bodyKey = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000017")); - AtomicReference captured = new AtomicReference<>(); - MutablePhysicsCommandContext buffer = new MutablePhysicsCommandContext(43L, 8L, 2); - - buffer.body(bodyKey, body -> { - captured.set(body); - body.setPosition(1.0f, 2.0f, 3.0f, true) - .applyForce(4.0f, 5.0f, 6.0f); - }); - - assertThrows(IllegalStateException.class, () -> captured.get().activate()); - assertEquals(2, buffer.freezeInternal(12L).publicBatch().commandCount()); - } - - @Test - void fluentDslRecordsSpawnCommandsWithoutManualCommandConstruction() { - RigidBodyKey bodyKey = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000003")); - SpaceId spaceId = new SpaceId(4); - Vector3f position = new Vector3f(2.0f, 3.0f, 4.0f); - MutablePhysicsCommandContext buffer = new MutablePhysicsCommandContext(44L, 9L); - - buffer.spawnBody(bodyKey, spawn -> spawn - .space(spaceId) - .box(0.5f, 0.6f, 0.7f) - .mass(2.0f) - .type(PhysicsBodyType.DYNAMIC) - .position(position) - .settings(RigidBodySpawnSettings.material(0.4f, 0.1f)) - .kind(PhysicsBodyKind.BODY) - .persistence(PhysicsBodyPersistenceMode.RUNTIME_ONLY)); - position.set(9.0f, 9.0f, 9.0f); - - PhysicsCommandBatch batch = buffer.freezeInternal(13L).publicBatch(); - - assertEquals(1, batch.commandCount()); - } - - @Test - void bareSpawnRecorderRecordsWhenContextFreezes() { - RigidBodyKey bodyKey = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000023")); - SpaceId spaceId = new SpaceId(4); - MutablePhysicsCommandContext buffer = new MutablePhysicsCommandContext(44L, 9L); - - buffer.spawnBody(bodyKey) - .space(spaceId) - .box(0.5f, 0.5f, 0.5f) - .position(1.0f, 2.0f, 3.0f); - - PhysicsCommandBatch batch = buffer.freezeInternal(13L).publicBatch(); - - assertEquals(1, batch.commandCount()); - } - - @Test - void singleSpawnRecipeScopesRecorderToCallback() { - RigidBodyKey bodyKey = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000021")); - SpaceId spaceId = new SpaceId(4); - AtomicReference captured = new AtomicReference<>(); - MutablePhysicsCommandContext buffer = new MutablePhysicsCommandContext(44L, 9L); - - buffer.spawnBody(bodyKey, spawn -> { - captured.set(spawn); - spawn.space(spaceId).box(0.5f, 0.5f, 0.5f); - }); - - assertThrows(IllegalStateException.class, () -> captured.get().mass(2.0f)); - assertEquals(1, buffer.freezeInternal(13L).publicBatch().commandCount()); - } - - @Test - void spawnRecipesSealCapturedRecordersWhenRecipeThrows() { - RigidBodyKey bodyKey = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000022")); - SpaceId spaceId = new SpaceId(4); - PhysicsShapeSpec box = PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f); - RigidBodySpawnSettings settings = RigidBodySpawnSettings.defaults(); - AtomicReference singleSpawn = new AtomicReference<>(); - AtomicReference bulkSpawns = new AtomicReference<>(); - AtomicReference templatedSpawns = new AtomicReference<>(); - MutablePhysicsCommandContext buffer = new MutablePhysicsCommandContext(44L, 9L); - - assertThrows(IllegalStateException.class, - () -> buffer.spawnBody(bodyKey, spawn -> { - singleSpawn.set(spawn); - throw new IllegalStateException("single spawn failed"); - })); - assertThrows(IllegalStateException.class, - () -> buffer.spawnBodies(1, spawns -> { - bulkSpawns.set(spawns); - throw new IllegalStateException("bulk spawn failed"); - })); - assertThrows(IllegalStateException.class, - () -> buffer.spawnBodies(1, - spaceId, - box, - 1.0f, - PhysicsBodyType.DYNAMIC, - settings, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY, - spawns -> { - templatedSpawns.set(spawns); - throw new IllegalStateException("templated spawn failed"); - })); - - assertThrows(IllegalStateException.class, () -> singleSpawn.get().space(spaceId)); - assertThrows(IllegalStateException.class, - () -> bulkSpawns.get().body(bodyKey, spawn -> spawn.space(spaceId).shape(box))); - assertThrows(IllegalStateException.class, - () -> templatedSpawns.get().body(bodyKey, 1.0f, 2.0f, 3.0f)); - } - - @Test - void fluentDslRecordsBulkSpawnCommandForRepeatedBodies() { - SpaceId spaceId = new SpaceId(5); - RigidBodyKey first = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000004")); - RigidBodyKey second = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000005")); - PhysicsShapeSpec box = PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f); - RigidBodySpawnSettings settings = RigidBodySpawnSettings.material(0.6f, 0.2f); - MutablePhysicsCommandContext buffer = new MutablePhysicsCommandContext(45L, 10L); - - buffer.spawnBodies(2, spawns -> spawns - .body(first, spawn -> spawn - .space(spaceId) - .shape(box) - .position(1.0f, 2.0f, 3.0f) - .settings(settings)) - .body(second, - spaceId, - box, - 1.0f, - PhysicsBodyType.DYNAMIC, - 4.0f, - 5.0f, - 6.0f, - settings, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY)); - - PhysicsCommandBatch batch = buffer.freezeInternal(14L).publicBatch(); - - assertEquals(1, batch.commandCount()); - } - - @Test - void fluentDslRecordsTemplatedBulkSpawnCommandForRepeatedBodies() { - SpaceId spaceId = new SpaceId(5); - RigidBodyKey first = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000014")); - RigidBodyKey second = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000015")); - PhysicsShapeSpec box = PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f); - RigidBodySpawnSettings settings = RigidBodySpawnSettings.material(0.6f, 0.2f); - MutablePhysicsCommandContext buffer = new MutablePhysicsCommandContext(45L, 10L); - - buffer.spawnBodies(2, - spaceId, - box, - 1.0f, - PhysicsBodyType.DYNAMIC, - settings, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY, - spawns -> spawns - .body(first, 1.0f, 2.0f, 3.0f) - .body(second, 4.0f, 5.0f, 6.0f)); - - PhysicsCommandBatch batch = buffer.freezeInternal(14L).publicBatch(); - - assertEquals(1, batch.commandCount()); - } - - @Test - void bulkSpawnRecorderRejectsMutationAfterRecipeReturns() { - SpaceId spaceId = new SpaceId(5); - RigidBodyKey bodyKey = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000009")); - PhysicsShapeSpec box = PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f); - RigidBodySpawnSettings settings = RigidBodySpawnSettings.defaults(); - AtomicReference captured = new AtomicReference<>(); - MutablePhysicsCommandContext buffer = new MutablePhysicsCommandContext(45L, 10L); - - buffer.spawnBodies(1, spawns -> { - captured.set(spawns); - spawns.body(bodyKey, - spaceId, - box, - 1.0f, - PhysicsBodyType.DYNAMIC, - 1.0f, - 2.0f, - 3.0f, - settings, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY); - }); - - assertThrows(IllegalStateException.class, - () -> captured.get().body(bodyKey, spawn -> spawn.space(spaceId).shape(box))); - assertEquals(1, buffer.freezeInternal(14L).publicBatch().commandCount()); - } - - @Test - void templatedBulkSpawnRecorderRejectsMutationAfterRecipeReturns() { - SpaceId spaceId = new SpaceId(5); - RigidBodyKey bodyKey = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000016")); - PhysicsShapeSpec box = PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f); - RigidBodySpawnSettings settings = RigidBodySpawnSettings.defaults(); - AtomicReference captured = new AtomicReference<>(); - MutablePhysicsCommandContext buffer = new MutablePhysicsCommandContext(45L, 10L); - - buffer.spawnBodies(1, - spaceId, - box, - 1.0f, - PhysicsBodyType.DYNAMIC, - settings, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY, - spawns -> { - captured.set(spawns); - spawns.body(bodyKey, 1.0f, 2.0f, 3.0f); - }); - - assertThrows(IllegalStateException.class, - () -> captured.get().body(bodyKey, 4.0f, 5.0f, 6.0f)); - assertEquals(1, buffer.freezeInternal(14L).publicBatch().commandCount()); - } - - @Test - void fluentDslRecordsJointCommandsWithoutManualCommandConstruction() { - SpaceId spaceId = new SpaceId(6); - JointKey jointKey = JointKey.of(UUID.fromString("00000000-0000-0000-0000-000000000006")); - RigidBodyKey bodyA = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000007")); - RigidBodyKey bodyB = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000008")); - Vector3f anchorA = new Vector3f(0.0f, -0.5f, 0.0f); - Vector3f anchorB = new Vector3f(0.0f, 0.5f, 0.0f); - Vector3f axis = new Vector3f(0.0f, 0.0f, 1.0f); - MutablePhysicsCommandContext buffer = new MutablePhysicsCommandContext(46L, 11L); - - buffer.joint(jointKey, joint -> joint - .space(spaceId) - .bodies(bodyA, bodyB) - .hinge(anchorA, anchorB, axis) - .limits(-0.75f, 0.75f) - .motor(1.25f, 2.5f)); - anchorA.set(9.0f, 9.0f, 9.0f); - anchorB.set(8.0f, 8.0f, 8.0f); - axis.set(7.0f, 7.0f, 7.0f); - - PhysicsCommandBatch batch = buffer.freezeInternal(15L).publicBatch(); - - assertEquals(1, batch.commandCount()); - } - - @Test - void bareJointRecorderRecordsWhenContextFreezes() { - SpaceId spaceId = new SpaceId(6); - JointKey jointKey = JointKey.of(UUID.fromString("00000000-0000-0000-0000-000000000024")); - RigidBodyKey bodyA = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000025")); - RigidBodyKey bodyB = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000026")); - MutablePhysicsCommandContext buffer = new MutablePhysicsCommandContext(46L, 11L); - - buffer.joint(jointKey) - .space(spaceId) - .bodies(bodyA, bodyB) - .fixed(0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f); - - PhysicsCommandBatch batch = buffer.freezeInternal(15L).publicBatch(); - - assertEquals(1, batch.commandCount()); - } - - @Test - void jointRecipeScopesRecorderToCallback() { - SpaceId spaceId = new SpaceId(6); - JointKey jointKey = JointKey.of(UUID.fromString("00000000-0000-0000-0000-000000000018")); - RigidBodyKey bodyA = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000019")); - RigidBodyKey bodyB = RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000020")); - AtomicReference captured = new AtomicReference<>(); - MutablePhysicsCommandContext buffer = new MutablePhysicsCommandContext(46L, 11L); - - buffer.joint(jointKey, joint -> { - captured.set(joint); - joint.space(spaceId) - .bodies(bodyA, bodyB) - .fixed(0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f); - }); - - assertThrows(IllegalStateException.class, () -> captured.get().motor(1.0f, 2.0f)); - assertEquals(1, buffer.freezeInternal(15L).publicBatch().commandCount()); - } - -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/simulation/RaycastClosestBatchQueryTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/simulation/RaycastClosestBatchQueryTest.java deleted file mode 100644 index f29584e7..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/simulation/RaycastClosestBatchQueryTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertSame; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import dev.hytalemodding.impulse.api.SpaceId; -import java.util.ArrayList; -import java.util.List; - -import dev.hytalemodding.impulse.core.plugin.simulation.query.RaycastClosestBatchQuery; -import org.junit.jupiter.api.Test; - -class RaycastClosestBatchQueryTest { - - @Test - void reusesImmutableSegmentsWhileFreezingTheInputList() { - RaycastSegment first = new RaycastSegment(1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f); - RaycastSegment second = new RaycastSegment(7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f); - List source = new ArrayList<>(List.of(first, second)); - - RaycastClosestBatchQuery query = new RaycastClosestBatchQuery(new SpaceId(3), source); - source.clear(); - - assertEquals(2, query.rayCount()); - assertSame(first, query.ray(0)); - assertSame(second, query.ray(1)); - assertThrows(UnsupportedOperationException.class, - () -> query.rays().add(new RaycastSegment(0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f))); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/simulation/RaycastHitViewTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/simulation/RaycastHitViewTest.java deleted file mode 100644 index ccce2121..00000000 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/simulation/RaycastHitViewTest.java +++ /dev/null @@ -1,49 +0,0 @@ -package dev.hytalemodding.impulse.core.plugin.simulation; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotSame; -import static org.junit.jupiter.api.Assertions.assertSame; - -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.core.plugin.simulation.view.RaycastHitView; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class RaycastHitViewTest { - - @Test - void storesGeometryAsScalarsWhileKeepingDefensiveVectorAccessors() { - Vector3f point = new Vector3f(1.0f, 2.0f, 3.0f); - Vector3f normal = new Vector3f(0.0f, 1.0f, 0.0f); - RaycastHitView view = new RaycastHitView(null, - PhysicsBodyType.DYNAMIC, - point, - normal, - ShapeType.BOX, - 0.25f, - 4.5f); - point.set(9.0f, 9.0f, 9.0f); - normal.set(8.0f, 8.0f, 8.0f); - - assertEquals(1.0f, view.pointX(), 0.00001f); - assertEquals(2.0f, view.pointY(), 0.00001f); - assertEquals(3.0f, view.pointZ(), 0.00001f); - assertEquals(0.0f, view.normalX(), 0.00001f); - assertEquals(1.0f, view.normalY(), 0.00001f); - assertEquals(0.0f, view.normalZ(), 0.00001f); - - Vector3f pointCopy = view.point(); - pointCopy.set(7.0f, 7.0f, 7.0f); - assertEquals(1.0f, view.pointX(), 0.00001f); - assertEquals(2.0f, view.pointY(), 0.00001f); - assertEquals(3.0f, view.pointZ(), 0.00001f); - assertNotSame(pointCopy, view.point()); - - Vector3f target = new Vector3f(); - assertSame(target, view.copyPointTo(target)); - assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), target); - assertSame(target, view.copyNormalTo(target)); - assertEquals(new Vector3f(0.0f, 1.0f, 0.0f), target); - } -} diff --git a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsSnapshotFrameTest.java b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsSnapshotFrameTest.java index aaf137ce..7995b8b0 100644 --- a/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsSnapshotFrameTest.java +++ b/impulse-core/src/test/java/dev/hytalemodding/impulse/core/plugin/snapshot/PublishedPhysicsSnapshotFrameTest.java @@ -8,9 +8,6 @@ import dev.hytalemodding.impulse.api.PhysicsBodyType; import dev.hytalemodding.impulse.api.ShapeType; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; import java.util.ArrayList; import java.util.List; import java.util.UUID; @@ -21,10 +18,10 @@ class PublishedPhysicsSnapshotFrameTest { - private static final RigidBodyKey BODY_ID = - RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000001")); - private static final RigidBodyKey SECOND_BODY_ID = - RigidBodyKey.of(UUID.fromString("00000000-0000-0000-0000-000000000002")); + private static final UUID BODY_ID = + UUID.fromString("00000000-0000-0000-0000-000000000001"); + private static final UUID SECOND_BODY_ID = + UUID.fromString("00000000-0000-0000-0000-000000000002"); private static final SpaceId SPACE_ID = new SpaceId(7); @Test @@ -40,8 +37,6 @@ void bodySnapshotCopiesPoseAndVelocityOnConstructionAndAccess() { 20L, 30L, 40L, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY, position, rotation, linearVelocity, @@ -99,8 +94,6 @@ void factoryCopiesExistingSnapshotData() { 2L, 3L, 4L, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT, ownerLaneSnapshot); ownerLaneSnapshot.position().zero(); ownerLaneSnapshot.rotation().identity(); @@ -112,8 +105,6 @@ void factoryCopiesExistingSnapshotData() { assertEquals(new Vector3f(0.0f, 2.0f, 0.0f), published.linearVelocity()); assertEquals(new Vector3f(0.0f, 0.0f, 3.0f), published.angularVelocity()); assertEquals(4L, published.registrationGeneration()); - assertEquals(PhysicsBodyKind.BODY, published.kind()); - assertEquals(PhysicsBodyPersistenceMode.PERSISTENT, published.persistenceMode()); assertEquals(PhysicsBodyType.KINEMATIC, published.bodyType()); assertEquals(ShapeType.BOX, published.shapeType()); assertEquals(new Vector3f(0.25f, 0.5f, 0.75f), published.boxHalfExtents()); @@ -154,8 +145,6 @@ void factoryDoesNotAliasThreadLocalScratchAcrossPublishedSnapshots() { 2L, 3L, 4L, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT, firstSnapshot); PublishedPhysicsBodySnapshot.from(BODY_ID, SPACE_ID, @@ -163,8 +152,6 @@ void factoryDoesNotAliasThreadLocalScratchAcrossPublishedSnapshots() { 2L, 2L, 2L, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT, secondSnapshot); assertEquals(new Vector3f(1.0f, 2.0f, 3.0f), first.position()); @@ -234,13 +221,13 @@ void framesVisitBodiesInPublicationOrder() { 20L, 30L, SPACE_ID); - List visited = getRigidBodyKeys(firstBody, secondBody); + List visited = getBodyUuids(firstBody, secondBody); assertEquals(List.of(BODY_ID, SECOND_BODY_ID), visited); } @NonNullDecl - private static List getRigidBodyKeys(PublishedPhysicsBodySnapshot firstBody, + private static List getBodyUuids(PublishedPhysicsBodySnapshot firstBody, PublishedPhysicsBodySnapshot secondBody) { PublishedPhysicsSnapshotFrame frame = new PublishedPhysicsSnapshotFrame(10L, 20L, @@ -255,9 +242,9 @@ private static List getRigidBodyKeys(PublishedPhysicsBodySnapshot 20L, 30L, List.of(firstBody, secondBody)))); - List visited = new ArrayList<>(); + List visited = new ArrayList<>(); - frame.forEachBody(body -> visited.add(body.bodyKey())); + frame.forEachBody(body -> visited.add(body.bodyUuid())); return visited; } @@ -316,7 +303,7 @@ private static PublishedPhysicsBodySnapshot bodySnapshot(long frameEpoch, return bodySnapshot(BODY_ID, frameEpoch, worldEpoch, spaceEpoch, spaceId); } - private static PublishedPhysicsBodySnapshot bodySnapshot(RigidBodyKey bodyId, + private static PublishedPhysicsBodySnapshot bodySnapshot(UUID bodyId, long frameEpoch, long worldEpoch, long spaceEpoch, @@ -327,8 +314,6 @@ private static PublishedPhysicsBodySnapshot bodySnapshot(RigidBodyKey bodyId, worldEpoch, spaceEpoch, 40L, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY, new Vector3f(1.0f, 2.0f, 3.0f), new Quaternionf(), new Vector3f(4.0f, 5.0f, 6.0f), diff --git a/impulse-early-plugin/build.gradle.kts b/impulse-early-plugin/build.gradle.kts new file mode 100644 index 00000000..0b80a1ee --- /dev/null +++ b/impulse-early-plugin/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + `java-library` +} + +version = rootProject.version + +val coreVineServerJar = project(":impulse-core").configurations.named("vineServerJar") +val coreVineServerJarFiles = files({ coreVineServerJar.get().files }) + +dependencies { + compileOnly(coreVineServerJarFiles) + compileOnly(libs.jsr305) + testCompileOnly(coreVineServerJarFiles) + testRuntimeOnly(coreVineServerJarFiles) +} diff --git a/impulse-early-plugin/src/main/java/com/hypixel/hytale/server/core/universe/world/storage/PhysicsStore.java b/impulse-early-plugin/src/main/java/com/hypixel/hytale/server/core/universe/world/storage/PhysicsStore.java new file mode 100644 index 00000000..1a5cfc03 --- /dev/null +++ b/impulse-early-plugin/src/main/java/com/hypixel/hytale/server/core/universe/world/storage/PhysicsStore.java @@ -0,0 +1,98 @@ +package com.hypixel.hytale.server.core.universe.world.storage; + +import com.hypixel.hytale.codec.store.CodecKey; +import com.hypixel.hytale.codec.store.CodecStore; +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.IResourceStorage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.WorldProvider; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public final class PhysicsStore implements WorldProvider { + + @Nonnull + public static final ComponentRegistry REGISTRY = new ComponentRegistry<>(); + + @Nonnull + public static final CodecKey> HOLDER_CODEC_KEY = + new CodecKey<>("PhysicsHolder"); + + static { + CodecStore.STATIC.putCodecSupplier(HOLDER_CODEC_KEY, REGISTRY::getEntityCodec); + } + + @Nonnull + private final World world; + @Nonnull + private final Map> refsByUuid = new ConcurrentHashMap<>(); + @Nullable + private Store store; + + public PhysicsStore(@Nonnull World world) { + this.world = Objects.requireNonNull(world, "world"); + } + + public synchronized void start(@Nonnull IResourceStorage resourceStorage) { + Objects.requireNonNull(resourceStorage, "resourceStorage"); + Store current = store; + if (current != null && !current.isShutdown()) { + throw new IllegalStateException("PhysicsStore is already started"); + } + store = REGISTRY.addStore(this, resourceStorage, addedStore -> store = addedStore); + } + + public synchronized void shutdown() { + Store current = requireStarted(); + if (!current.isShutdown()) { + current.shutdown(); + } + store = null; + refsByUuid.clear(); + } + + @Nonnull + public Store getStore() { + return requireStarted(); + } + + @Nonnull + @Override + public World getWorld() { + return world; + } + + @Nullable + public Ref getRefFromUUID(@Nonnull UUID uuid) { + return refsByUuid.get(Objects.requireNonNull(uuid, "uuid")); + } + + public void putRefForUUID(@Nonnull UUID uuid, @Nonnull Ref ref) { + refsByUuid.put(Objects.requireNonNull(uuid, "uuid"), Objects.requireNonNull(ref, "ref")); + } + + public void removeRefForUUID(@Nonnull UUID uuid, @Nonnull Ref ref) { + refsByUuid.remove(Objects.requireNonNull(uuid, "uuid"), Objects.requireNonNull(ref, "ref")); + } + + public void clearUuidIndex() { + refsByUuid.clear(); + } + + @Nonnull + private synchronized Store requireStarted() { + Store current = store; + if (current == null || current.isShutdown()) { + throw new IllegalStateException("PhysicsStore is not started; the Impulse early plugin " + + "must transform World before core physics-store systems run"); + } + return current; + } +} diff --git a/impulse-early-plugin/src/main/java/dev/hytalemodding/impulse/early/PhysicsStoreEarlyTransformer.java b/impulse-early-plugin/src/main/java/dev/hytalemodding/impulse/early/PhysicsStoreEarlyTransformer.java new file mode 100644 index 00000000..82f54b61 --- /dev/null +++ b/impulse-early-plugin/src/main/java/dev/hytalemodding/impulse/early/PhysicsStoreEarlyTransformer.java @@ -0,0 +1,602 @@ +package dev.hytalemodding.impulse.early; + +import com.hypixel.hytale.plugin.early.ClassTransformer; +import java.lang.classfile.ClassBuilder; +import java.lang.classfile.ClassElement; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassModel; +import java.lang.classfile.ClassTransform; +import java.lang.classfile.CodeBuilder; +import java.lang.classfile.CodeElement; +import java.lang.classfile.CodeTransform; +import java.lang.classfile.MethodModel; +import java.lang.classfile.TypeKind; +import java.lang.classfile.instruction.FieldInstruction; +import java.lang.classfile.instruction.InvokeInstruction; +import java.lang.classfile.instruction.ReturnInstruction; +import java.lang.constant.ClassDesc; +import java.lang.constant.ConstantDescs; +import java.lang.constant.MethodTypeDesc; +import java.util.ArrayList; +import javax.annotation.Nonnull; + +public final class PhysicsStoreEarlyTransformer implements ClassTransformer { + + private static final String PLUGIN_BASE_NAME = + "com.hypixel.hytale.server.core.plugin.PluginBase"; + private static final String WORLD_NAME = + "com.hypixel.hytale.server.core.universe.world.World"; + private static final String WORLD_CONFIG_SAVE_SYSTEM_NAME = + "com.hypixel.hytale.server.core.universe.system.WorldConfigSaveSystem"; + + private static final String PLUGIN_SHUTDOWN_TASKS_FIELD = "shutdownTasks"; + private static final String WORLD_CHUNK_STORE_FIELD = "chunkStore"; + private static final String WORLD_IS_TICKING_FIELD = "isTicking"; + private static final String WORLD_IS_PAUSED_FIELD = "isPaused"; + private static final String WORLD_STORE_FIELD = "physicsStore"; + private static final String WORLD_PATCH_MARKER_FIELD = "impulse$physicsStoreLifecyclePatched"; + private static final String WORLD_SAVE_PATCH_MARKER_FIELD = + "impulse$physicsStoreResourceSavePatched"; + private static final String PLUGIN_REGISTRY_METHOD = "getPhysicsStoreRegistry"; + private static final String WORLD_STORE_METHOD = "getPhysicsStore"; + private static final String SAVE_WORLD_CONFIG_AND_RESOURCES_METHOD = + "saveWorldConfigAndResources"; + + private static final ClassFile CLASS_FILE = ClassFile.of(); + private static final ClassDesc CD_CHUNK_STORE = + ClassDesc.of("com.hypixel.hytale.server.core.universe.world.storage.ChunkStore"); + private static final ClassDesc CD_COMPLETABLE_FUTURE = + ClassDesc.of("java.util.concurrent.CompletableFuture"); + private static final ClassDesc CD_COMPONENT_REGISTRY = + ClassDesc.of("com.hypixel.hytale.component.ComponentRegistry"); + private static final ClassDesc CD_COMPONENT_REGISTRY_PROXY = + ClassDesc.of("com.hypixel.hytale.component.ComponentRegistryProxy"); + private static final ClassDesc CD_COPY_ON_WRITE_ARRAY_LIST = + ClassDesc.of("java.util.concurrent.CopyOnWriteArrayList"); + private static final ClassDesc CD_ENTITY_STORE = + ClassDesc.of("com.hypixel.hytale.server.core.universe.world.storage.EntityStore"); + private static final ClassDesc CD_I_RESOURCE_STORAGE = + ClassDesc.of("com.hypixel.hytale.component.IResourceStorage"); + private static final ClassDesc CD_LIST = ClassDesc.of("java.util.List"); + private static final ClassDesc CD_PATH = ClassDesc.of("java.nio.file.Path"); + private static final ClassDesc CD_PHYSICS_STORE = + ClassDesc.of("com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore"); + private static final ClassDesc CD_PHYSICS_STORE_HOOKS = + ClassDesc.of("dev.hytalemodding.impulse.early.PhysicsStoreHooks"); + private static final ClassDesc CD_PHYSICS_STORE_WORLD = + ClassDesc.of("dev.hytalemodding.impulse.early.PhysicsStoreWorld"); + private static final ClassDesc CD_PLUGIN_BASE = + ClassDesc.of("com.hypixel.hytale.server.core.plugin.PluginBase"); + private static final ClassDesc CD_STORE = ClassDesc.of("com.hypixel.hytale.component.Store"); + private static final ClassDesc CD_UNIVERSE = + ClassDesc.of("com.hypixel.hytale.server.core.universe.Universe"); + private static final ClassDesc CD_WORLD = + ClassDesc.of("com.hypixel.hytale.server.core.universe.world.World"); + private static final ClassDesc CD_WORLD_CONFIG = + ClassDesc.of("com.hypixel.hytale.server.core.universe.world.WorldConfig"); + private static final ClassDesc CD_WORLD_CONFIG_PROVIDER = + ClassDesc.of("com.hypixel.hytale.server.core.universe.world.WorldConfigProvider"); + + private static final MethodTypeDesc MTD_BOOLEAN = MethodTypeDesc.of(ConstantDescs.CD_boolean); + private static final MethodTypeDesc MTD_CHUNK_STORE = MethodTypeDesc.of(CD_CHUNK_STORE); + private static final MethodTypeDesc MTD_COMPONENT_REGISTRY_PROXY = + MethodTypeDesc.of(CD_COMPONENT_REGISTRY_PROXY); + private static final MethodTypeDesc MTD_COMPONENT_REGISTRY_PROXY_INIT = + MethodTypeDesc.of(ConstantDescs.CD_void, CD_LIST, CD_COMPONENT_REGISTRY); + private static final MethodTypeDesc MTD_COMPLETABLE_FUTURE = + MethodTypeDesc.of(CD_COMPLETABLE_FUTURE); + private static final MethodTypeDesc MTD_COMPLETABLE_FUTURE_ARRAY_TO_FUTURE = + MethodTypeDesc.of(CD_COMPLETABLE_FUTURE, CD_COMPLETABLE_FUTURE.arrayType()); + private static final MethodTypeDesc MTD_ENTITY_STORE = MethodTypeDesc.of(CD_ENTITY_STORE); + private static final MethodTypeDesc MTD_PHYSICS_STORE = MethodTypeDesc.of(CD_PHYSICS_STORE); + private static final MethodTypeDesc MTD_PHYSICS_STORE_INIT = + MethodTypeDesc.of(ConstantDescs.CD_void, CD_WORLD); + private static final MethodTypeDesc MTD_PHYSICS_START = + MethodTypeDesc.of(ConstantDescs.CD_void, CD_PHYSICS_STORE, CD_I_RESOURCE_STORAGE); + private static final MethodTypeDesc MTD_PHYSICS_TICK = + MethodTypeDesc.of(ConstantDescs.CD_void, + CD_PHYSICS_STORE, + ConstantDescs.CD_float, + ConstantDescs.CD_boolean, + ConstantDescs.CD_boolean); + private static final MethodTypeDesc MTD_PHYSICS_SAVE = + MethodTypeDesc.of(CD_COMPLETABLE_FUTURE, CD_PHYSICS_STORE); + private static final MethodTypeDesc MTD_PHYSICS_SHUTDOWN = + MethodTypeDesc.of(ConstantDescs.CD_void, CD_PHYSICS_STORE); + private static final MethodTypeDesc MTD_SAVE_WORLD_CONFIG_AND_RESOURCES = + MethodTypeDesc.of(CD_COMPLETABLE_FUTURE, CD_WORLD); + private static final MethodTypeDesc MTD_STORE = MethodTypeDesc.of(CD_STORE); + private static final MethodTypeDesc MTD_UNIVERSE = MethodTypeDesc.of(CD_UNIVERSE); + private static final MethodTypeDesc MTD_VOID = MethodTypeDesc.of(ConstantDescs.CD_void); + private static final MethodTypeDesc MTD_VOID_FLOAT = + MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_float); + private static final MethodTypeDesc MTD_VOID_RESOURCE_STORAGE = + MethodTypeDesc.of(ConstantDescs.CD_void, CD_I_RESOURCE_STORAGE); + private static final MethodTypeDesc MTD_WORLD_CONFIG = MethodTypeDesc.of(CD_WORLD_CONFIG); + private static final MethodTypeDesc MTD_WORLD_CONFIG_PROVIDER = + MethodTypeDesc.of(CD_WORLD_CONFIG_PROVIDER); + private static final MethodTypeDesc MTD_WORLD_CONFIG_PROVIDER_SAVE = + MethodTypeDesc.of(CD_COMPLETABLE_FUTURE, CD_PATH, CD_WORLD_CONFIG, CD_WORLD); + private static final MethodTypeDesc MTD_PATH = MethodTypeDesc.of(CD_PATH); + + @Override + public int priority() { + return 100; + } + + @Override + public byte[] transform(String name, String transformedName, byte[] bytes) { + String target = transformedName != null ? transformedName : name; + if (matches(target, PLUGIN_BASE_NAME)) { + return transformPluginBase(bytes); + } + if (matches(target, WORLD_NAME)) { + return transformWorld(bytes); + } + if (matches(target, WORLD_CONFIG_SAVE_SYSTEM_NAME)) { + return transformWorldConfigSaveSystem(bytes); + } + return bytes; + } + + @Nonnull + private static byte[] transformPluginBase(@Nonnull byte[] bytes) { + ClassModel model = CLASS_FILE.parse(bytes); + if (hasMethod(model.methods(), PLUGIN_REGISTRY_METHOD, MTD_COMPONENT_REGISTRY_PROXY)) { + return bytes; + } + return CLASS_FILE.transformClass(model, ClassTransform.ACCEPT_ALL.andThen( + ClassTransform.endHandler(PhysicsStoreEarlyTransformer::addPluginRegistryAccessor))); + } + + @Nonnull + private static byte[] transformWorld(@Nonnull byte[] bytes) { + ClassModel model = CLASS_FILE.parse(bytes); + boolean patchMarkerPresent = hasField(model.fields(), + WORLD_PATCH_MARKER_FIELD, + ConstantDescs.CD_boolean); + boolean interfacePresent = hasInterface(model, CD_PHYSICS_STORE_WORLD); + if (patchMarkerPresent) { + if (interfacePresent) { + return bytes; + } + return CLASS_FILE.transformClass(model, + ClassTransform.ACCEPT_ALL.andThen(ClassTransform.endHandler(builder -> + addPhysicsStoreWorldInterface(model, builder)))); + } + boolean fieldPresent = hasField(model.fields(), WORLD_STORE_FIELD, CD_PHYSICS_STORE); + boolean methodPresent = hasMethod(model.methods(), WORLD_STORE_METHOD, MTD_PHYSICS_STORE); + var initializePhysicsStore = new InitializePhysicsStoreTransform(); + var startPhysicsStore = new StartPhysicsStoreTransform(); + var tickPhysicsStore = new TickPhysicsStoreTransform(); + var shutdownPhysicsStore = new ShutdownPhysicsStoreTransform(); + ClassTransform transform = ClassTransform.ACCEPT_ALL + .andThen(ClassTransform.transformingMethodBodies( + PhysicsStoreEarlyTransformer::isWorldConstructor, + initializePhysicsStore)) + .andThen(ClassTransform.transformingMethodBodies( + PhysicsStoreEarlyTransformer::isWorldStartMethod, + startPhysicsStore)) + .andThen(ClassTransform.transformingMethodBodies( + PhysicsStoreEarlyTransformer::isWorldTickMethod, + tickPhysicsStore)) + .andThen(ClassTransform.transformingMethodBodies( + PhysicsStoreEarlyTransformer::isWorldShutdownMethod, + shutdownPhysicsStore)) + .andThen(ClassTransform.endHandler(builder -> { + if (!fieldPresent) { + builder.withField(WORLD_STORE_FIELD, CD_PHYSICS_STORE, ClassFile.ACC_PRIVATE); + } + if (!methodPresent) { + addWorldStoreAccessor(builder); + } + if (!interfacePresent) { + addPhysicsStoreWorldInterface(model, builder); + } + builder.withField(WORLD_PATCH_MARKER_FIELD, + ConstantDescs.CD_boolean, + ClassFile.ACC_PRIVATE | ClassFile.ACC_STATIC | ClassFile.ACC_FINAL + | ClassFile.ACC_SYNTHETIC); + })); + byte[] transformed = CLASS_FILE.transformClass(model, transform); + requireTransformApplied(WORLD_NAME, "constructor PhysicsStore initialization", + initializePhysicsStore.injected()); + requireTransformApplied(WORLD_NAME, "onStart PhysicsStore start hook", + startPhysicsStore.injected()); + requireTransformApplied(WORLD_NAME, "tick PhysicsStore tick hook", + tickPhysicsStore.injected()); + requireTransformApplied(WORLD_NAME, "onShutdown PhysicsStore shutdown hook", + shutdownPhysicsStore.injected()); + return transformed; + } + + @Nonnull + private static byte[] transformWorldConfigSaveSystem(@Nonnull byte[] bytes) { + ClassModel model = CLASS_FILE.parse(bytes); + if (hasField(model.fields(), WORLD_SAVE_PATCH_MARKER_FIELD, ConstantDescs.CD_boolean)) { + return bytes; + } + var replaceSave = new ReplaceSaveWorldConfigAndResourcesTransform(); + ClassTransform transform = ClassTransform.ACCEPT_ALL + .andThen(ClassTransform.transformingMethodBodies( + PhysicsStoreEarlyTransformer::isSaveWorldConfigAndResourcesMethod, + replaceSave)) + .andThen(ClassTransform.endHandler(builder -> builder.withField( + WORLD_SAVE_PATCH_MARKER_FIELD, + ConstantDescs.CD_boolean, + ClassFile.ACC_PRIVATE | ClassFile.ACC_STATIC | ClassFile.ACC_FINAL + | ClassFile.ACC_SYNTHETIC))); + byte[] transformed = CLASS_FILE.transformClass(model, transform); + requireTransformApplied(WORLD_CONFIG_SAVE_SYSTEM_NAME, + "saveWorldConfigAndResources resource save hook", + replaceSave.replaced()); + return transformed; + } + + private static void addPluginRegistryAccessor(@Nonnull ClassBuilder builder) { + builder.withMethodBody(PLUGIN_REGISTRY_METHOD, + MTD_COMPONENT_REGISTRY_PROXY, + ClassFile.ACC_PUBLIC, + code -> code.new_(CD_COMPONENT_REGISTRY_PROXY) + .dup() + .aload(0) + .getfield(CD_PLUGIN_BASE, PLUGIN_SHUTDOWN_TASKS_FIELD, CD_COPY_ON_WRITE_ARRAY_LIST) + .getstatic(CD_PHYSICS_STORE, "REGISTRY", CD_COMPONENT_REGISTRY) + .invokespecial(CD_COMPONENT_REGISTRY_PROXY, + "", + MTD_COMPONENT_REGISTRY_PROXY_INIT) + .areturn()); + } + + private static void addWorldStoreAccessor(@Nonnull ClassBuilder builder) { + builder.withMethodBody(WORLD_STORE_METHOD, + MTD_PHYSICS_STORE, + ClassFile.ACC_PUBLIC, + code -> code.aload(0) + .getfield(CD_WORLD, WORLD_STORE_FIELD, CD_PHYSICS_STORE) + .areturn()); + } + + private static void addPhysicsStoreWorldInterface(@Nonnull ClassModel model, + @Nonnull ClassBuilder builder) { + ArrayList interfaces = new ArrayList<>(model.interfaces().size() + 1); + for (var entry : model.interfaces()) { + interfaces.add(entry.asSymbol()); + } + if (!interfaces.contains(CD_PHYSICS_STORE_WORLD)) { + interfaces.add(CD_PHYSICS_STORE_WORLD); + } + builder.withInterfaceSymbols(interfaces); + } + + private static final class InitializePhysicsStoreTransform implements CodeTransform { + + private boolean injected; + + @Override + public void accept(CodeBuilder builder, CodeElement element) { + if (element instanceof ReturnInstruction returnInstruction + && returnInstruction.typeKind() == TypeKind.VOID) { + emitInitializePhysicsStore(builder); + injected = true; + } + builder.with(element); + } + + boolean injected() { + return injected; + } + } + + private static final class StartPhysicsStoreTransform implements CodeTransform { + + private boolean injected; + + @Override + public void accept(CodeBuilder builder, CodeElement element) { + builder.with(element); + if (!injected && isEntityStoreStartInvoke(element)) { + emitStartPhysicsStore(builder); + injected = true; + } + } + + boolean injected() { + return injected; + } + } + + private static final class TickPhysicsStoreTransform implements CodeTransform { + + private boolean pendingChunkStore; + private boolean chunkStoreTickSeen; + private boolean injected; + + @Override + public void accept(CodeBuilder builder, CodeElement element) { + if (!injected && chunkStoreTickSeen && isWorldConsumeTaskQueueInvoke(element)) { + emitTickPhysicsStore(builder); + injected = true; + } + builder.with(element); + if (isWorldChunkStoreField(element)) { + pendingChunkStore = true; + } else if (pendingChunkStore && isStoreTickInvoke(element)) { + chunkStoreTickSeen = true; + pendingChunkStore = false; + } + } + + boolean injected() { + return injected; + } + } + + private static final class ShutdownPhysicsStoreTransform implements CodeTransform { + + private boolean injected; + + @Override + public void accept(CodeBuilder builder, CodeElement element) { + if (element instanceof ReturnInstruction returnInstruction + && returnInstruction.typeKind() == TypeKind.VOID) { + emitShutdownPhysicsStore(builder); + injected = true; + } + builder.with(element); + } + + boolean injected() { + return injected; + } + } + + private static final class ReplaceSaveWorldConfigAndResourcesTransform implements CodeTransform { + + private boolean replaced; + + @Override + public void atStart(CodeBuilder builder) { + emitSaveWorldConfigAndResources(builder); + replaced = true; + } + + @Override + public void accept(CodeBuilder builder, CodeElement element) { + } + + boolean replaced() { + return replaced; + } + } + + private static void emitInitializePhysicsStore(@Nonnull CodeBuilder code) { + code.aload(0) + .new_(CD_PHYSICS_STORE) + .dup() + .aload(0) + .invokespecial(CD_PHYSICS_STORE, "", MTD_PHYSICS_STORE_INIT) + .putfield(CD_WORLD, WORLD_STORE_FIELD, CD_PHYSICS_STORE); + } + + private static void emitStartPhysicsStore(@Nonnull CodeBuilder code) { + code.aload(0) + .getfield(CD_WORLD, WORLD_STORE_FIELD, CD_PHYSICS_STORE) + .aload(1) + .invokestatic(CD_PHYSICS_STORE_HOOKS, "start", MTD_PHYSICS_START); + } + + private static void emitTickPhysicsStore(@Nonnull CodeBuilder code) { + code.aload(0) + .getfield(CD_WORLD, WORLD_STORE_FIELD, CD_PHYSICS_STORE) + .fload(1) + .aload(0) + .getfield(CD_WORLD, WORLD_IS_TICKING_FIELD, ConstantDescs.CD_boolean) + .aload(0) + .getfield(CD_WORLD, WORLD_IS_PAUSED_FIELD, ConstantDescs.CD_boolean) + .invokestatic(CD_PHYSICS_STORE_HOOKS, "tickAfterChunk", MTD_PHYSICS_TICK); + } + + private static void emitShutdownPhysicsStore(@Nonnull CodeBuilder code) { + code.aload(0) + .getfield(CD_WORLD, WORLD_STORE_FIELD, CD_PHYSICS_STORE) + .invokestatic(CD_PHYSICS_STORE_HOOKS, "shutdown", MTD_PHYSICS_SHUTDOWN); + } + + private static void emitSaveWorldConfigAndResources(@Nonnull CodeBuilder code) { + var resourceOnly = code.newLabel(); + code.aload(0) + .invokevirtual(CD_WORLD, "getWorldConfig", MTD_WORLD_CONFIG) + .astore(1) + .aload(1) + .invokevirtual(CD_WORLD_CONFIG, "isSavingConfig", MTD_BOOLEAN) + .ifeq(resourceOnly) + .aload(1) + .invokevirtual(CD_WORLD_CONFIG, "consumeHasChanged", MTD_BOOLEAN) + .ifeq(resourceOnly) + .iconst_4() + .anewarray(CD_COMPLETABLE_FUTURE) + .dup() + .iconst_0(); + emitChunkStoreSave(code); + code.aastore() + .dup() + .iconst_1(); + emitEntityStoreSave(code); + code.aastore() + .dup() + .iconst_2(); + emitPhysicsStoreSave(code); + code.aastore() + .dup() + .iconst_3(); + emitWorldConfigSave(code); + code.aastore() + .invokestatic(CD_COMPLETABLE_FUTURE, + "allOf", + MTD_COMPLETABLE_FUTURE_ARRAY_TO_FUTURE) + .areturn() + .labelBinding(resourceOnly) + .iconst_3() + .anewarray(CD_COMPLETABLE_FUTURE) + .dup() + .iconst_0(); + emitChunkStoreSave(code); + code.aastore() + .dup() + .iconst_1(); + emitEntityStoreSave(code); + code.aastore() + .dup() + .iconst_2(); + emitPhysicsStoreSave(code); + code.aastore() + .invokestatic(CD_COMPLETABLE_FUTURE, + "allOf", + MTD_COMPLETABLE_FUTURE_ARRAY_TO_FUTURE) + .areturn(); + } + + private static void emitChunkStoreSave(@Nonnull CodeBuilder code) { + code.aload(0) + .invokevirtual(CD_WORLD, "getChunkStore", MTD_CHUNK_STORE) + .invokevirtual(CD_CHUNK_STORE, "getStore", MTD_STORE) + .invokevirtual(CD_STORE, "saveAllResources", MTD_COMPLETABLE_FUTURE); + } + + private static void emitEntityStoreSave(@Nonnull CodeBuilder code) { + code.aload(0) + .invokevirtual(CD_WORLD, "getEntityStore", MTD_ENTITY_STORE) + .invokevirtual(CD_ENTITY_STORE, "getStore", MTD_STORE) + .invokevirtual(CD_STORE, "saveAllResources", MTD_COMPLETABLE_FUTURE); + } + + private static void emitPhysicsStoreSave(@Nonnull CodeBuilder code) { + code.aload(0) + .invokevirtual(CD_WORLD, WORLD_STORE_METHOD, MTD_PHYSICS_STORE) + .invokestatic(CD_PHYSICS_STORE_HOOKS, "saveResources", MTD_PHYSICS_SAVE); + } + + private static void emitWorldConfigSave(@Nonnull CodeBuilder code) { + code.invokestatic(CD_UNIVERSE, "get", MTD_UNIVERSE) + .invokevirtual(CD_UNIVERSE, "getWorldConfigProvider", MTD_WORLD_CONFIG_PROVIDER) + .aload(0) + .invokevirtual(CD_WORLD, "getSavePath", MTD_PATH) + .aload(0) + .invokevirtual(CD_WORLD, "getWorldConfig", MTD_WORLD_CONFIG) + .aload(0) + .invokeinterface(CD_WORLD_CONFIG_PROVIDER, "save", MTD_WORLD_CONFIG_PROVIDER_SAVE); + } + + private static boolean isWorldConstructor(@Nonnull MethodModel method) { + return method.methodName().equalsString("") + && method.methodType().equalsString("(Ljava/lang/String;Ljava/nio/file/Path;" + + "Lcom/hypixel/hytale/server/core/universe/world/WorldConfig;)V"); + } + + private static boolean isWorldStartMethod(@Nonnull MethodModel method) { + return method.methodName().equalsString("onStart") + && method.methodType().equalsString("()V"); + } + + private static boolean isWorldTickMethod(@Nonnull MethodModel method) { + return method.methodName().equalsString("tick") + && method.methodType().equalsString("(F)V"); + } + + private static boolean isWorldShutdownMethod(@Nonnull MethodModel method) { + return method.methodName().equalsString("onShutdown") + && method.methodType().equalsString("()V"); + } + + private static boolean isSaveWorldConfigAndResourcesMethod(@Nonnull MethodModel method) { + return method.methodName().equalsString(SAVE_WORLD_CONFIG_AND_RESOURCES_METHOD) + && method.methodType().equalsString( + "(Lcom/hypixel/hytale/server/core/universe/world/World;)" + + "Ljava/util/concurrent/CompletableFuture;"); + } + + private static boolean isWorldChunkStoreField(@Nonnull CodeElement element) { + return element instanceof FieldInstruction instruction + && instruction.owner().asSymbol().equals(CD_WORLD) + && instruction.name().equalsString(WORLD_CHUNK_STORE_FIELD) + && instruction.typeSymbol().equals(CD_CHUNK_STORE); + } + + private static boolean isStoreTickInvoke(@Nonnull CodeElement element) { + return element instanceof InvokeInstruction instruction + && instruction.owner().asSymbol().equals(CD_STORE) + && (instruction.name().equalsString("tick") + || instruction.name().equalsString("pausedTick")) + && instruction.typeSymbol().equals(MTD_VOID_FLOAT); + } + + private static boolean isWorldConsumeTaskQueueInvoke(@Nonnull CodeElement element) { + return element instanceof InvokeInstruction instruction + && instruction.owner().asSymbol().equals(CD_WORLD) + && instruction.name().equalsString("consumeTaskQueue") + && instruction.typeSymbol().equals(MTD_VOID); + } + + private static boolean isEntityStoreStartInvoke(@Nonnull CodeElement element) { + return element instanceof InvokeInstruction instruction + && instruction.owner().asSymbol().equals(CD_ENTITY_STORE) + && instruction.name().equalsString("start") + && instruction.typeSymbol().equals(MTD_VOID_RESOURCE_STORAGE); + } + + private static boolean isChunkStoreShutdownInvoke(@Nonnull CodeElement element) { + return element instanceof InvokeInstruction instruction + && instruction.owner().asSymbol().equals(CD_CHUNK_STORE) + && instruction.name().equalsString("shutdown") + && instruction.typeSymbol().equals(MTD_VOID); + } + + private static void requireTransformApplied(@Nonnull String target, + @Nonnull String hook, + boolean applied) { + if (!applied) { + throw new IllegalStateException("Could not patch " + hook + " in " + target + + "; Impulse PhysicsStore early plugin requires the Hytale 0.6.0-pre.3 " + + "server lifecycle bytecode shape"); + } + } + + private static boolean hasField(@Nonnull Iterable elements, + @Nonnull String name, + @Nonnull ClassDesc descriptor) { + for (ClassElement element : elements) { + if (element instanceof java.lang.classfile.FieldModel field + && field.fieldName().equalsString(name) + && field.fieldType().equalsString(descriptor.descriptorString())) { + return true; + } + } + return false; + } + + private static boolean hasMethod(@Nonnull Iterable methods, + @Nonnull String name, + @Nonnull MethodTypeDesc descriptor) { + for (MethodModel method : methods) { + if (method.methodName().equalsString(name) + && method.methodType().equalsString(descriptor.descriptorString())) { + return true; + } + } + return false; + } + + private static boolean hasInterface(@Nonnull ClassModel model, @Nonnull ClassDesc descriptor) { + for (var classEntry : model.interfaces()) { + if (classEntry.matches(descriptor)) { + return true; + } + } + return false; + } + + private static boolean matches(@Nonnull String name, @Nonnull String target) { + return target.equals(name) || target.replace('.', '/').equals(name); + } +} diff --git a/impulse-early-plugin/src/main/java/dev/hytalemodding/impulse/early/PhysicsStoreHooks.java b/impulse-early-plugin/src/main/java/dev/hytalemodding/impulse/early/PhysicsStoreHooks.java new file mode 100644 index 00000000..4118c182 --- /dev/null +++ b/impulse-early-plugin/src/main/java/dev/hytalemodding/impulse/early/PhysicsStoreHooks.java @@ -0,0 +1,134 @@ +package dev.hytalemodding.impulse.early; + +import com.hypixel.hytale.component.IResourceStorage; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.function.Consumer; +import java.util.function.Function; +import javax.annotation.Nonnull; + +public final class PhysicsStoreHooks { + + @Nonnull + private static final Set> SHUTDOWN_HOOKS = + new CopyOnWriteArraySet<>(); + @Nonnull + private static final Set>> SAVE_HOOKS = + new CopyOnWriteArraySet<>(); + @Nonnull + private static final Set TICK_GATES = new CopyOnWriteArraySet<>(); + + private PhysicsStoreHooks() { + } + + public static void registerShutdownHook(@Nonnull Consumer hook) { + SHUTDOWN_HOOKS.add(Objects.requireNonNull(hook, "hook")); + } + + public static void unregisterShutdownHook(@Nonnull Consumer hook) { + SHUTDOWN_HOOKS.remove(Objects.requireNonNull(hook, "hook")); + } + + public static void registerSaveHook( + @Nonnull Function> hook) { + SAVE_HOOKS.add(Objects.requireNonNull(hook, "hook")); + } + + public static void unregisterSaveHook( + @Nonnull Function> hook) { + SAVE_HOOKS.remove(Objects.requireNonNull(hook, "hook")); + } + + public static void registerTickGate(@Nonnull TickGate gate) { + TICK_GATES.add(Objects.requireNonNull(gate, "gate")); + } + + public static void unregisterTickGate(@Nonnull TickGate gate) { + TICK_GATES.remove(Objects.requireNonNull(gate, "gate")); + } + + public static void start(@Nonnull PhysicsStore physicsStore, + @Nonnull IResourceStorage resourceStorage) { + Objects.requireNonNull(physicsStore, "physicsStore") + .start(Objects.requireNonNull(resourceStorage, "resourceStorage")); + } + + public static void tickAfterChunk(@Nonnull PhysicsStore physicsStore, + float dt, + boolean ticking, + boolean paused) { + PhysicsStore checked = Objects.requireNonNull(physicsStore, "physicsStore"); + if (!shouldTick(checked, dt)) { + return; + } + Store store = checked.getStore(); + if (ticking && !paused) { + store.tick(dt); + return; + } + store.pausedTick(dt); + } + + @Nonnull + public static CompletableFuture saveResources(@Nonnull PhysicsStore physicsStore) { + PhysicsStore checked = Objects.requireNonNull(physicsStore, "physicsStore"); + CompletableFuture[] futures = new CompletableFuture[SAVE_HOOKS.size() + 1]; + int index = 0; + for (Function> hook : SAVE_HOOKS) { + try { + futures[index++] = Objects.requireNonNull(hook.apply(checked), + "save hook future"); + } catch (RuntimeException exception) { + futures[index++] = CompletableFuture.failedFuture(exception); + } + } + futures[index++] = checked.getStore().saveAllResources(); + return CompletableFuture.allOf(futures); + } + + public static void shutdown(@Nonnull PhysicsStore physicsStore) { + PhysicsStore checked = Objects.requireNonNull(physicsStore, "physicsStore"); + RuntimeException hookFailure = null; + for (Consumer hook : SHUTDOWN_HOOKS) { + try { + hook.accept(checked); + } catch (RuntimeException exception) { + if (hookFailure == null) { + hookFailure = exception; + } else { + hookFailure.addSuppressed(exception); + } + } + } + try { + checked.shutdown(); + } catch (RuntimeException exception) { + if (hookFailure != null) { + exception.addSuppressed(hookFailure); + } + throw exception; + } + if (hookFailure != null) { + throw hookFailure; + } + } + + private static boolean shouldTick(@Nonnull PhysicsStore physicsStore, float dt) { + for (TickGate gate : TICK_GATES) { + if (!gate.shouldTick(physicsStore, dt)) { + return false; + } + } + return true; + } + + @FunctionalInterface + public interface TickGate { + + boolean shouldTick(@Nonnull PhysicsStore physicsStore, float dt); + } +} diff --git a/impulse-early-plugin/src/main/java/dev/hytalemodding/impulse/early/PhysicsStoreWorld.java b/impulse-early-plugin/src/main/java/dev/hytalemodding/impulse/early/PhysicsStoreWorld.java new file mode 100644 index 00000000..367831dd --- /dev/null +++ b/impulse-early-plugin/src/main/java/dev/hytalemodding/impulse/early/PhysicsStoreWorld.java @@ -0,0 +1,13 @@ +package dev.hytalemodding.impulse.early; + +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import javax.annotation.Nonnull; + +/** + * Compile-time contract implemented by World after the Impulse early plugin transform. + */ +public interface PhysicsStoreWorld { + + @Nonnull + PhysicsStore getPhysicsStore(); +} diff --git a/impulse-early-plugin/src/main/resources/META-INF/services/com.hypixel.hytale.plugin.early.ClassTransformer b/impulse-early-plugin/src/main/resources/META-INF/services/com.hypixel.hytale.plugin.early.ClassTransformer new file mode 100644 index 00000000..c841000d --- /dev/null +++ b/impulse-early-plugin/src/main/resources/META-INF/services/com.hypixel.hytale.plugin.early.ClassTransformer @@ -0,0 +1 @@ +dev.hytalemodding.impulse.early.PhysicsStoreEarlyTransformer diff --git a/impulse-examples/build.gradle.kts b/impulse-examples/build.gradle.kts index d5be9f04..02b09c57 100644 --- a/impulse-examples/build.gradle.kts +++ b/impulse-examples/build.gradle.kts @@ -7,9 +7,15 @@ plugins { version = rootProject.version dependencies { - implementation(project(":impulse-api")) + implementation(project(":impulse-backends:api")) compileOnly(project(":impulse-core")) + compileOnly(project(":impulse-builtins:control")) + compileOnly(project(":impulse-early-plugin")) testImplementation(project(":impulse-core")) + testImplementation(project(":impulse-builtins:control")) + testImplementation(project(":impulse-early-plugin")) + testImplementation(testFixtures(project(":impulse-backends:api"))) + testImplementation(libs.objenesis) testCompileOnly("com.hypixel.hytale:Server:${property("hytale_version") as String}") testRuntimeOnly("com.hypixel.hytale:Server:${property("hytale_version") as String}") compileOnly(libs.lombok) @@ -37,5 +43,10 @@ hytaleTools { modUrl = property("mod_website") as String modDescription = "Example plugins for Impulse" manifestServerVersion = property("hytale_version") as String - manifestDependencies = "HytaleModding:Impulse=*" + manifestDependencies = listOf( + "HytaleModding:Impulse=*", + "HytaleModding:ImpulseControl=*", + "HytaleModding:ImpulsePhysicsEntity=*", + "HytaleModding:ImpulsePhysicsChunk=*" + ).joinToString(",") } diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/DirectionalPendulumsCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/DirectionalPendulumsCommand.java new file mode 100644 index 00000000..df4765f8 --- /dev/null +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/DirectionalPendulumsCommand.java @@ -0,0 +1,697 @@ +package dev.hytalemodding.impulse.examples.commands; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; +import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncPlayerCommand; +import com.hypixel.hytale.server.core.modules.debug.DebugUtils; +import com.hypixel.hytale.server.core.modules.time.TimeResource; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.EventTitleUtil; +import dev.hytalemodding.impulse.api.*; +import dev.hytalemodding.impulse.api.runtime.PhysicsBackendRuntimeProvider; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointType; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsChunkCollisionSettings; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodyEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsJointEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsSpaces; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSolverSettings; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils.CreatedBlockBody; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Vector3d; +import org.joml.Vector3f; + +public final class DirectionalPendulumsCommand extends AbstractAsyncPlayerCommand { + + private static final float GRAVITY = 9.81f; + private static final float PIVOT_HALF_SIZE = 0.25f; + private static final float LINK_HALF_LENGTH = 1.0f; + private static final float LINK_THICKNESS = 0.18f; + private static final float UPPER_SWING_SPEED = 0.9f; + private static final float LOWER_RELATIVE_SWING_SPEED = -0.55f; + private static final float CINEMATIC_UPPER_SWING_SPEED = 2.1f; + private static final float CINEMATIC_LOWER_RELATIVE_SWING_SPEED = -1.2f; + static final String PENDULUM_BLOCK_TYPE = "Rock_Marble"; + static final String CINEMATIC_JOLT_BLOCK_TYPE = "Rock_Aqua"; + static final String CINEMATIC_RAPIER_BLOCK_TYPE = "Rock_Basalt"; + static final float PENDULUM_VISUAL_ORIGIN_OFFSET_Y = 0.5f; + static final float PENDULUM_SLEEP_LINEAR_THRESHOLD = 0.0f; + static final float PENDULUM_SLEEP_ANGULAR_THRESHOLD = 0.0f; + static final float PENDULUM_TIME_UNTIL_SLEEP = Float.MAX_VALUE; + private static final float CINEMATIC_GRAVITY_ARROW_DURATION_SECONDS = 12.0f; + private static final BackendId JOLT_BACKEND_ID = new BackendId("impulse:jolt"); + private static final BackendId RAPIER_BACKEND_ID = new BackendId("impulse:rapier"); + private static final RigidBodySpawnSettings PENDULUM_BODY_SETTINGS = + RigidBodySpawnSettings.material(0.6f, 0.05f) + .withCollisionFilter(PhysicsCollisionFilters.DYNAMIC_BODY, + PhysicsCollisionFilters.TERRAIN); + + private static final DirectionSpec[] DIRECTIONS = { + new DirectionSpec(new Vector3f(0.0f, -GRAVITY, 0.0f), + new Vector3d(-8.0, 0.0, -8.0), + new Vector3d(1.0, 0.0, 0.0), + BackendSlot.JOLT), + new DirectionSpec(new Vector3f(0.0f, GRAVITY, 0.0f), + new Vector3d(8.0, 0.0, -8.0), + new Vector3d(1.0, 0.0, 0.0), + BackendSlot.JOLT), + new DirectionSpec(new Vector3f(GRAVITY, 0.0f, 0.0f), + new Vector3d(-8.0, 0.0, 8.0), + new Vector3d(0.0, 0.0, 1.0), + BackendSlot.RAPIER), + new DirectionSpec(new Vector3f(-GRAVITY, 0.0f, 0.0f), + new Vector3d(8.0, 0.0, 8.0), + new Vector3d(0.0, 0.0, 1.0), + BackendSlot.RAPIER) + }; + + private static final DirectionSpec[] CINEMATIC_DIRECTIONS = { + new DirectionSpec(new Vector3f(0.0f, -GRAVITY, 0.0f), + new Vector3d(-4.0, 0.0, -4.0), + new Vector3d(1.0, 0.0, 0.0), + BackendSlot.JOLT), + new DirectionSpec(new Vector3f(0.0f, GRAVITY, 0.0f), + new Vector3d(4.0, 0.0, -4.0), + new Vector3d(1.0, 0.0, 0.0), + BackendSlot.JOLT), + new DirectionSpec(new Vector3f(GRAVITY, 0.0f, 0.0f), + new Vector3d(-4.0, 0.0, 4.0), + new Vector3d(0.0, 0.0, 1.0), + BackendSlot.RAPIER), + new DirectionSpec(new Vector3f(-GRAVITY, 0.0f, 0.0f), + new Vector3d(4.0, 0.0, 4.0), + new Vector3d(0.0, 0.0, 1.0), + BackendSlot.RAPIER) + }; + + private final OptionalArg presetArg = this.withOptionalArg( + "preset", + "Preset: default, clip, cinematic, or gravity-compass", + ArgTypes.STRING); + + public DirectionalPendulumsCommand() { + super("directional-pendulums", + "Spawn four non-streaming spaces with directional double pendulums"); + } + + @Nonnull + @Override + protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, + @Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PlayerRef playerRef, + @Nonnull World world) { + Preset preset = parsePreset(ctx); + if (preset == null) { + return CompletableFuture.completedFuture(null); + } + + BackendSelection backendSelection = resolveBackendSelection(ctx); + if (backendSelection == null) { + return CompletableFuture.completedFuture(null); + } + + Store physicsStore = PhysicsThreading.store(world); + Vector3d playerPosition = new Vector3d(playerRef.getTransform().getPosition()); + Vector3d center = playerPosition.add(0.0, 6.0, 8.0); + try { + SpawnResult result = spawnDemo(physicsStore, backendSelection, center, preset); + TimeResource time = store.getResource(TimeResource.getResourceType()); + for (CreatedBlockBody createdBody : result.createdBodies()) { + ExamplePhysicsUtils.attachBlockBody(store, time, createdBody); + } + if (preset == Preset.CINEMATIC) { + try { + showGravityCompassPresentation(world, playerRef, result); + } catch (RuntimeException presentationException) { + ctx.sender().sendMessage(Message.raw("Spawned Gravity Compass, but cinematic " + + "presentation failed: " + presentationException.getMessage())); + } + } + ctx.sender().sendMessage(Message.raw("Spawned four directional double pendulum spaces " + + "with physicsChunk=none, backends=2x " + backendSelection.joltBackendId().value() + + "/2x " + backendSelection.rapierBackendId().value() + + ", preset=" + preset.label() + + ", blocks=" + preset.blockSummary() + + ": " + spaceIds(result) + ".")); + } catch (RuntimeException exception) { + ctx.sender().sendMessage(Message.raw("Failed to spawn directional pendulums: " + + exception.getMessage())); + } + return CompletableFuture.completedFuture(null); + } + + @Nonnull + static SpawnResult spawnDemo(@Nonnull Store physicsStore, + @Nonnull BackendId backendId, + @Nonnull Vector3d center) { + return spawnDemo(physicsStore, backendId, center, Preset.DEFAULT); + } + + @Nonnull + static SpawnResult spawnDemo(@Nonnull Store physicsStore, + @Nonnull BackendId backendId, + @Nonnull Vector3d center, + @Nonnull Preset preset) { + return spawnDemo(physicsStore, + new BackendSelection(backendId, backendId), + center, + preset); + } + + @Nonnull + static SpawnResult spawnDemo(@Nonnull Store physicsStore, + @Nonnull BackendId joltBackendId, + @Nonnull BackendId rapierBackendId, + @Nonnull Vector3d center) { + return spawnDemo(physicsStore, joltBackendId, rapierBackendId, center, Preset.DEFAULT); + } + + @Nonnull + static SpawnResult spawnDemo(@Nonnull Store physicsStore, + @Nonnull BackendId joltBackendId, + @Nonnull BackendId rapierBackendId, + @Nonnull Vector3d center, + @Nonnull Preset preset) { + return spawnDemo(physicsStore, + new BackendSelection(joltBackendId, rapierBackendId), + center, + preset); + } + + @Nonnull + private static SpawnResult spawnDemo(@Nonnull Store physicsStore, + @Nonnull BackendSelection backendSelection, + @Nonnull Vector3d center, + @Nonnull Preset preset) { + PhysicsThreading.requireWorldThread(physicsStore, + "spawn directional double pendulum spaces"); + DirectionSpec[] directions = preset.directions(); + List pendulums = new ArrayList<>(directions.length); + List createdBodies = new ArrayList<>(directions.length * 3); + for (DirectionSpec direction : directions) { + BackendId backendId = backendSelection.backendId(direction.backendSlot()); + SpaceId spaceId = PhysicsSpaces.create(physicsStore, backendId); + Ref spaceRef = PhysicsSpaces.resolveRef(physicsStore, spaceId); + if (spaceRef == null) { + throw new IllegalStateException("Created physics space id=" + spaceId.value() + + " is not bound."); + } + putChunkCollisionNone(physicsStore, spaceRef); + putGravity(physicsStore, spaceRef, direction.gravity()); + putNoSleepSolverSettings(physicsStore, spaceRef); + CreatedPendulum pendulum = spawnPendulum(physicsStore, + spaceId, + spaceRef, + backendId, + direction.gravity(), + new Vector3d(center).add(direction.originOffset()), + direction.tangent(), + preset.blockType(direction.backendSlot()), + preset.upperSwingSpeed(), + preset.lowerRelativeSwingSpeed()); + pendulums.add(pendulum); + createdBodies.add(pendulum.anchor()); + createdBodies.add(pendulum.upper()); + createdBodies.add(pendulum.lower()); + } + return new SpawnResult(pendulums, createdBodies); + } + + @Nonnull + private static CreatedPendulum spawnPendulum(@Nonnull Store physicsStore, + @Nonnull SpaceId spaceId, + @Nonnull Ref spaceRef, + @Nonnull BackendId backendId, + @Nonnull Vector3f gravity, + @Nonnull Vector3d anchorPosition, + @Nonnull Vector3d tangent, + @Nonnull String blockType, + float upperSwingSpeed, + float lowerRelativeSwingSpeed) { + Vector3d down = new Vector3d(gravity.x, gravity.y, gravity.z).normalize(); + Vector3d upperPosition = new Vector3d(anchorPosition) + .add(new Vector3d(down).mul(LINK_HALF_LENGTH)); + Vector3d lowerPosition = new Vector3d(anchorPosition) + .add(new Vector3d(down).mul(LINK_HALF_LENGTH * 3.0)); + InitialVelocity upperVelocity = initialVelocity(down, + tangent, + upperSwingSpeed, + upperSwingSpeed); + InitialVelocity lowerVelocity = initialVelocity(down, + tangent, + upperSwingSpeed * 2.0f + lowerRelativeSwingSpeed, + lowerRelativeSwingSpeed); + + CreatedBlockBody anchor = spawnBody(physicsStore, + spaceRef, + spaceId, + anchorPosition, + PhysicsShapeSpec.box(PIVOT_HALF_SIZE, PIVOT_HALF_SIZE, PIVOT_HALF_SIZE), + PhysicsBodyType.STATIC, + 0.0f, + null, + blockType); + CreatedBlockBody upper = spawnBody(physicsStore, + spaceRef, + spaceId, + upperPosition, + linkShape(down), + PhysicsBodyType.DYNAMIC, + 1.0f, + upperVelocity, + blockType); + CreatedBlockBody lower = spawnBody(physicsStore, + spaceRef, + spaceId, + lowerPosition, + linkShape(down), + PhysicsBodyType.DYNAMIC, + 1.0f, + lowerVelocity, + blockType); + Ref topJointRef = addPointJoint(physicsStore, + spaceRef, + anchor, + upper, + new Vector3f(), + anchor(down, -LINK_HALF_LENGTH)); + Ref middleJointRef = addPointJoint(physicsStore, + spaceRef, + upper, + lower, + anchor(down, LINK_HALF_LENGTH), + anchor(down, -LINK_HALF_LENGTH)); + return new CreatedPendulum(spaceId, + spaceRef, + backendId, + gravity, + anchor, + upper, + lower, + topJointRef, + middleJointRef); + } + + @Nonnull + private static CreatedBlockBody spawnBody(@Nonnull Store physicsStore, + @Nonnull Ref spaceRef, + @Nonnull SpaceId spaceId, + @Nonnull Vector3d position, + @Nonnull PhysicsShapeSpec shape, + @Nonnull PhysicsBodyType bodyType, + float mass, + @Nullable InitialVelocity initialVelocity, + @Nonnull String blockType) { + UUID bodyUuid = UUID.randomUUID(); + Ref bodyRef = physicsStore.addEntity(PhysicsBodyEntities.bodyHolder( + spaceRef, + bodyUuid, + ExamplePhysicsUtils.toVector3f(position), + shape, + bodyType, + mass, + PENDULUM_BODY_SETTINGS, + initialVelocity != null ? initialVelocity.linear() : null), + AddReason.SPAWN); + assert bodyRef != null; + if (initialVelocity != null) { + TargetComponent target = physicsStore.getComponent(bodyRef, + TargetComponent.getComponentType()); + if (target == null) { + throw new IllegalStateException("PhysicsStore body is missing target component: " + + bodyRef); + } + TargetComponent updated = target.clone(); + updated.setAngularVelocity(initialVelocity.angular()); + physicsStore.putComponent(bodyRef, TargetComponent.getComponentType(), updated); + } + return new CreatedBlockBody(bodyUuid, + bodyRef, + spaceId, + blockType, + (float) position.x, + (float) position.y, + (float) position.z, + mass > 0.0f, + PENDULUM_VISUAL_ORIGIN_OFFSET_Y); + } + + @Nonnull + private static InitialVelocity initialVelocity(@Nonnull Vector3d down, + @Nonnull Vector3d tangent, + float centerSpeed, + float angularEndpointSpeed) { + Vector3d angular = new Vector3d(down) + .cross(tangent) + .mul(angularEndpointSpeed / LINK_HALF_LENGTH); + return new InitialVelocity(ExamplePhysicsUtils.toVector3f( + new Vector3d(tangent).mul(centerSpeed)), + ExamplePhysicsUtils.toVector3f(angular)); + } + + @Nonnull + private static Ref addPointJoint(@Nonnull Store physicsStore, + @Nonnull Ref spaceRef, + @Nonnull CreatedBlockBody bodyA, + @Nonnull CreatedBlockBody bodyB, + @Nonnull Vector3f anchorA, + @Nonnull Vector3f anchorB) { + JointComponent joint = PhysicsJointEntities.joint(spaceRef, + bodyA.bodyRef(), + bodyB.bodyRef(), + JointType.POINT, + anchorA, + anchorB, + new Vector3f()); + Ref jointRef = physicsStore.addEntity(PhysicsEntities.jointHolder( + physicsStore, + UUID.randomUUID(), + joint), + AddReason.SPAWN); + assert jointRef != null; + return jointRef; + } + + @Nonnull + private static Vector3f anchor(@Nonnull Vector3d down, float scale) { + return new Vector3f((float) down.x, (float) down.y, (float) down.z).mul(scale); + } + + @Nonnull + private static PhysicsShapeSpec linkShape(@Nonnull Vector3d down) { + float x = Math.abs(down.x) > 0.5 ? LINK_HALF_LENGTH : LINK_THICKNESS; + float y = Math.abs(down.y) > 0.5 ? LINK_HALF_LENGTH : LINK_THICKNESS; + float z = Math.abs(down.z) > 0.5 ? LINK_HALF_LENGTH : LINK_THICKNESS; + return PhysicsShapeSpec.box(x, y, z); + } + + private static void putChunkCollisionNone(@Nonnull Store physicsStore, + @Nonnull Ref spaceRef) { + PhysicsChunkCollisionSettings settings = new PhysicsChunkCollisionSettings(); + settings.setMode(PhysicsChunkCollisionMode.NONE); + PhysicsSpaces.putChunkCollisionSettings(physicsStore, spaceRef, settings); + } + + private static void putGravity(@Nonnull Store physicsStore, + @Nonnull Ref spaceRef, + @Nonnull Vector3f gravity) { + SpaceComponent space = PhysicsSpaces.getSpaceComponent(physicsStore, + spaceRef, + SpaceComponent.getComponentType()); + if (space == null) { + throw new IllegalStateException("PhysicsStore entity is not a space entity: " + + spaceRef); + } + SpaceComponent updated = space.clone(); + updated.setGravity(gravity); + PhysicsSpaces.putSpaceComponent(physicsStore, + spaceRef, + SpaceComponent.getComponentType(), + updated); + } + + private static void putNoSleepSolverSettings(@Nonnull Store physicsStore, + @Nonnull Ref spaceRef) { + PhysicsSolverSettings settings = PhysicsSpaces.solverSettings(physicsStore, spaceRef); + if (settings == null) { + throw new IllegalStateException("PhysicsStore entity is not a space entity: " + + spaceRef); + } + settings.setDynamicSleepTuning(PENDULUM_SLEEP_LINEAR_THRESHOLD, + PENDULUM_SLEEP_ANGULAR_THRESHOLD, + PENDULUM_TIME_UNTIL_SLEEP); + PhysicsSpaces.putSolverSettings(physicsStore, spaceRef, settings); + } + + @Nullable + private Preset parsePreset(@Nonnull CommandContext ctx) { + if (!presetArg.provided(ctx)) { + return Preset.DEFAULT; + } + String rawPreset = presetArg.get(ctx); + Preset preset = Preset.from(rawPreset); + if (preset != null) { + return preset; + } + ctx.sender().sendMessage(Message.raw("Unknown directional pendulums preset '" + + rawPreset + "'. Expected default, clip, cinematic, or gravity-compass.")); + return null; + } + + private static void showGravityCompassPresentation(@Nonnull World world, + @Nonnull PlayerRef playerRef, + @Nonnull SpawnResult result) { + EventTitleUtil.showEventTitleToPlayer(playerRef, + Message.raw("Gravity Compass"), + Message.raw("Jolt cyan x2 / Rapier basalt x2, four gravity vectors"), + true); + for (CreatedPendulum pendulum : result.pendulums()) { + drawGravityArrow(world, pendulum); + } + } + + private static void drawGravityArrow(@Nonnull World world, + @Nonnull CreatedPendulum pendulum) { + Vector3d direction = new Vector3d(pendulum.gravity().x, + pendulum.gravity().y, + pendulum.gravity().z).normalize().mul(2.5); + Vector3d origin = bodyPosition(pendulum.anchor()) + .sub(new Vector3d(direction).mul(0.5)); + DebugUtils.addArrow(world, + origin, + direction, + gravityArrowColor(pendulum), + 1.0f, + CINEMATIC_GRAVITY_ARROW_DURATION_SECONDS, + DebugUtils.FLAG_FADE); + } + + @Nonnull + private static Vector3f gravityArrowColor(@Nonnull CreatedPendulum pendulum) { + return CINEMATIC_JOLT_BLOCK_TYPE.equals(pendulum.anchor().blockType()) + ? DebugUtils.COLOR_CYAN + : DebugUtils.COLOR_MAGENTA; + } + + @Nonnull + private static Vector3d bodyPosition(@Nonnull CreatedBlockBody body) { + return new Vector3d(body.positionX(), body.positionY(), body.positionZ()); + } + + @Nullable + private BackendSelection resolveBackendSelection(@Nonnull CommandContext ctx) { + List missing = new ArrayList<>(2); + if (!backendRegistered(JOLT_BACKEND_ID)) { + missing.add(JOLT_BACKEND_ID); + } + if (!backendRegistered(RAPIER_BACKEND_ID)) { + missing.add(RAPIER_BACKEND_ID); + } + if (missing.isEmpty()) { + return new BackendSelection(JOLT_BACKEND_ID, RAPIER_BACKEND_ID); + } + String missingIds = String.join(", ", + missing.stream().map(BackendId::value).toList()); + ctx.sender().sendMessage(Message.raw("Directional pendulums require registered backends " + + missingIds + ". Available backends: " + availableBackendIds())); + return null; + } + + private static boolean backendRegistered(@Nonnull BackendId backendId) { + try { + ImpulseBackendRegistry.getRuntimeProvider(backendId); + return true; + } catch (RuntimeException exception) { + return false; + } + } + + @Nonnull + private static String availableBackendIds() { + List backendIds = new ArrayList<>(); + for (PhysicsBackendRuntimeProvider provider : ImpulseBackendRegistry.getRuntimeProviders()) { + backendIds.add(provider.getId().value()); + } + backendIds.sort(String::compareTo); + return backendIds.isEmpty() ? "" : String.join(", ", backendIds); + } + + @Nonnull + private static String spaceIds(@Nonnull SpawnResult result) { + List ids = result.pendulums() + .stream() + .map(pendulum -> Integer.toString(pendulum.spaceId().value())) + .toList(); + return String.join(", ", ids); + } + + record SpawnResult(@Nonnull List pendulums, + @Nonnull List createdBodies) { + + SpawnResult { + pendulums = List.copyOf(Objects.requireNonNull(pendulums, "pendulums")); + createdBodies = List.copyOf(Objects.requireNonNull(createdBodies, "createdBodies")); + } + } + + record CreatedPendulum(@Nonnull SpaceId spaceId, + @Nonnull Ref spaceRef, + @Nonnull BackendId backendId, + @Nonnull Vector3f gravity, + @Nonnull CreatedBlockBody anchor, + @Nonnull CreatedBlockBody upper, + @Nonnull CreatedBlockBody lower, + @Nonnull Ref topJointRef, + @Nonnull Ref middleJointRef) { + + CreatedPendulum { + Objects.requireNonNull(spaceId, "spaceId"); + Objects.requireNonNull(spaceRef, "spaceRef"); + Objects.requireNonNull(backendId, "backendId"); + gravity = new Vector3f(Objects.requireNonNull(gravity, "gravity")); + Objects.requireNonNull(anchor, "anchor"); + Objects.requireNonNull(upper, "upper"); + Objects.requireNonNull(lower, "lower"); + Objects.requireNonNull(topJointRef, "topJointRef"); + Objects.requireNonNull(middleJointRef, "middleJointRef"); + } + } + + private record DirectionSpec(@Nonnull Vector3f gravity, + @Nonnull Vector3d originOffset, + @Nonnull Vector3d tangent, + @Nonnull BackendSlot backendSlot) { + + private DirectionSpec { + gravity = new Vector3f(Objects.requireNonNull(gravity, "gravity")); + originOffset = new Vector3d(Objects.requireNonNull(originOffset, "originOffset")); + tangent = new Vector3d(Objects.requireNonNull(tangent, "tangent")).normalize(); + Objects.requireNonNull(backendSlot, "backendSlot"); + } + } + + private record BackendSelection(@Nonnull BackendId joltBackendId, + @Nonnull BackendId rapierBackendId) { + + private BackendSelection { + Objects.requireNonNull(joltBackendId, "joltBackendId"); + Objects.requireNonNull(rapierBackendId, "rapierBackendId"); + } + + @Nonnull + private BackendId backendId(@Nonnull BackendSlot backendSlot) { + return switch (backendSlot) { + case JOLT -> joltBackendId; + case RAPIER -> rapierBackendId; + }; + } + } + + enum Preset { + DEFAULT("default"), + CINEMATIC("clip"); + + private final String label; + + Preset(@Nonnull String label) { + this.label = label; + } + + @Nullable + private static Preset from(@Nullable String rawPreset) { + if (rawPreset == null || rawPreset.isBlank()) { + return DEFAULT; + } + return switch (rawPreset.trim().toLowerCase(Locale.ROOT)) { + case "default", "classic" -> DEFAULT; + case "clip", "cinematic", "gravity-compass", "gravity_compass" -> CINEMATIC; + default -> null; + }; + } + + @Nonnull + private String label() { + return label; + } + + @Nonnull + private DirectionSpec[] directions() { + return switch (this) { + case DEFAULT -> DIRECTIONS; + case CINEMATIC -> CINEMATIC_DIRECTIONS; + }; + } + + @Nonnull + private String blockType(@Nonnull BackendSlot backendSlot) { + return switch (this) { + case DEFAULT -> PENDULUM_BLOCK_TYPE; + case CINEMATIC -> switch (backendSlot) { + case JOLT -> CINEMATIC_JOLT_BLOCK_TYPE; + case RAPIER -> CINEMATIC_RAPIER_BLOCK_TYPE; + }; + }; + } + + private float upperSwingSpeed() { + return switch (this) { + case DEFAULT -> UPPER_SWING_SPEED; + case CINEMATIC -> CINEMATIC_UPPER_SWING_SPEED; + }; + } + + private float lowerRelativeSwingSpeed() { + return switch (this) { + case DEFAULT -> LOWER_RELATIVE_SWING_SPEED; + case CINEMATIC -> CINEMATIC_LOWER_RELATIVE_SWING_SPEED; + }; + } + + @Nonnull + private String blockSummary() { + return switch (this) { + case DEFAULT -> PENDULUM_BLOCK_TYPE; + case CINEMATIC -> "Jolt=" + CINEMATIC_JOLT_BLOCK_TYPE + + ", Rapier=" + CINEMATIC_RAPIER_BLOCK_TYPE; + }; + } + } + + private enum BackendSlot { + JOLT, + RAPIER + } + + private record InitialVelocity(@Nonnull Vector3f linear, + @Nonnull Vector3f angular) { + + private InitialVelocity { + linear = new Vector3f(Objects.requireNonNull(linear, "linear")); + angular = new Vector3f(Objects.requireNonNull(angular, "angular")); + } + } +} diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/DropCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/DropCommand.java index 11ba9bbc..926e46ed 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/DropCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/DropCommand.java @@ -1,6 +1,7 @@ package dev.hytalemodding.impulse.examples.commands; -import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.Holder; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.Message; @@ -8,26 +9,36 @@ import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; import com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncPlayerCommand; -import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.entity.entities.BlockEntity; +import com.hypixel.hytale.server.core.modules.entity.DespawnComponent; +import com.hypixel.hytale.server.core.modules.physics.component.Velocity; import com.hypixel.hytale.server.core.modules.time.TimeResource; import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.builtin.control.ImpulseControllableComponent; +import dev.hytalemodding.impulse.builtin.control.PhysicsControlSessions; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsEntityAttachments; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodyEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; +import dev.hytalemodding.impulse.examples.utils.ExampleBlockEntityVisuals; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; +import org.joml.Quaternionf; import org.joml.Vector3d; +import org.joml.Vector3f; /** - * Spawn a visible block entity that falls under Bullet physics. + * Spawn a PhysicsStore body row with an attached visible block entity. */ public class DropCommand extends AbstractAsyncPlayerCommand { - private static final ComponentType TRANSFORM_TYPE = - TransformComponent.getComponentType(); private final OptionalArg blockTypeArg = this.withOptionalArg( "blockType", "Hytale block type used for the attached visual entity", @@ -48,34 +59,44 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - TransformComponent playerTransform = store.getComponent(ref, TRANSFORM_TYPE); - - if (playerTransform == null) { - ctx.sender().sendMessage(Message.raw("Cannot determine player position.")); - return CompletableFuture.completedFuture(null); - } - - Vector3d playerPos = playerTransform.getPosition(); + Vector3d playerPos = playerRef.getTransform().getPosition(); float spawnX = (float) playerPos.x(); float spawnY = (float) playerPos.y() + 5f; float spawnZ = (float) playerPos.z(); + Vector3d position = new Vector3d(spawnX, spawnY, spawnZ); - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); - SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, resource, spaceArg); - if (spaceId == null) { + ExamplePhysicsUtils.SpaceSelection space = ExamplePhysicsUtils.spaceSelection(ctx, + world, + spaceArg); + if (space == null) { return CompletableFuture.completedFuture(null); } TimeResource time = store.getResource(TimeResource.getResourceType()); - ExamplePhysicsUtils.spawnBlockBody(store, - time, - resource, - spaceId, - new Vector3d(spawnX, spawnY, spawnZ), - blockType(ctx), + Store physicsStore = PhysicsThreading.store(world); + PhysicsThreading.requireWorldThread(physicsStore, + "spawn an example PhysicsStore body entity"); + UUID bodyUuid = UUID.randomUUID(); + Holder bodyHolder = PhysicsBodyEntities.dynamicBodyHolder(space.spaceRef(), + bodyUuid, + toVector3f(position), PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f), 1.0f, - RigidBodySpawnSettings.material(0.5f, 0.5f)); + RigidBodySpawnSettings.material(0.5f, 0.5f), + null); + Ref bodyRef = physicsStore.addEntity(bodyHolder, AddReason.SPAWN); + + assert bodyRef != null; + store.addEntity(attachedPhysicsBlockEntityHolder(time, + bodyRef, + bodyUuid, + blockType(ctx), + position, + new Vector3f(), + new Quaternionf(), + Float.NaN, + true), + AddReason.SPAWN); ctx.sender() .sendMessage(Message.raw("Dropped box at " + spawnX + ", " + spawnY + ", " + spawnZ)); @@ -86,7 +107,50 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull private String blockType(@Nonnull CommandContext ctx) { return blockTypeArg.provided(ctx) - ? ExamplePhysicsUtils.resolveBlockType(blockTypeArg.get(ctx)) + ? ExampleBlockEntityVisuals.resolveBlockType(blockTypeArg.get(ctx)) : ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE; } + + @Nonnull + private static Holder attachedPhysicsBlockEntityHolder(@Nonnull TimeResource time, + @Nonnull Ref bodyRef, + @Nonnull UUID bodyUuid, + @Nonnull String blockType, + @Nonnull Vector3d position, + @Nonnull Vector3f localPositionOffset, + @Nonnull Quaternionf localRotationOffset, + float visualOriginOffsetY, + boolean controllable) { + requirePhysicsEntityVisuals(); + Holder holder = BlockEntity.assembleDefaultBlockEntity(time, + ExampleBlockEntityVisuals.resolveBlockType(blockType), + new Vector3d(position)); + holder.tryRemoveComponent(DespawnComponent.getComponentType()); + holder.tryRemoveComponent(Velocity.getComponentType()); + + BodyAttachmentComponent attachment = BodyAttachmentComponent.impulseOwnedVisual(bodyUuid, + localPositionOffset, + localRotationOffset, + visualOriginOffsetY); + attachment.setBodyRef(bodyRef); + holder.addComponent(BodyAttachmentComponent.getComponentType(), attachment); + if (controllable && PhysicsControlSessions.isAvailable()) { + holder.addComponent(ImpulseControllableComponent.getComponentType(), + new ImpulseControllableComponent()); + } + return holder; + } + + private static void requirePhysicsEntityVisuals() { + if (!PhysicsEntityAttachments.isAvailable()) { + throw new IllegalStateException( + "Impulse PhysicsEntity integration is not available. " + + "Enable HytaleModding:ImpulsePhysicsEntity to spawn entity-backed example visuals."); + } + } + + @Nonnull + private static Vector3f toVector3f(@Nonnull Vector3d vector) { + return new Vector3f((float) vector.x, (float) vector.y, (float) vector.z); + } } diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ExamplePhysicsOriginMath.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ExamplePhysicsOriginMath.java deleted file mode 100644 index ad8bfe1c..00000000 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ExamplePhysicsOriginMath.java +++ /dev/null @@ -1,22 +0,0 @@ -package dev.hytalemodding.impulse.examples.commands; - -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import java.util.Objects; -import javax.annotation.Nonnull; -import org.joml.Vector3d; - -final class ExamplePhysicsOriginMath { - - private ExamplePhysicsOriginMath() { - } - - @Nonnull - static Vector3d visualPositionFromBodyCenter(@Nonnull Vector3d bodyCenter, - @Nonnull PhysicsShapeSpec shape) { - Objects.requireNonNull(bodyCenter, "bodyCenter"); - Objects.requireNonNull(shape, "shape"); - return new Vector3d(bodyCenter.x, - bodyCenter.y - shape.centerOfMassOffsetY(), - bodyCenter.z); - } -} diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ExamplePhysicsUtils.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ExamplePhysicsUtils.java deleted file mode 100644 index 4ab4484e..00000000 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ExamplePhysicsUtils.java +++ /dev/null @@ -1,972 +0,0 @@ -package dev.hytalemodding.impulse.examples.commands; - -import com.hypixel.hytale.component.AddReason; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Holder; -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.math.vector.Rotation3f; -import com.hypixel.hytale.math.vector.Vector3dUtil; -import com.hypixel.hytale.server.core.Message; -import com.hypixel.hytale.server.core.asset.type.model.config.Model; -import com.hypixel.hytale.server.core.command.system.CommandContext; -import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; -import com.hypixel.hytale.server.core.modules.entity.component.HeadRotation; -import com.hypixel.hytale.server.core.modules.entity.component.ModelComponent; -import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; -import com.hypixel.hytale.server.core.modules.time.TimeResource; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.modules.control.ImpulseControllableComponent; -import dev.hytalemodding.impulse.core.plugin.modules.control.PhysicsControlSessions; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyCollisionComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyDynamicsComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyIdentityComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyKinematicTargetComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyMaterialComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyShapeComponent; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsVisualMaterializationSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandHandle; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.PhysicsCommandRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import java.util.Arrays; -import java.util.Comparator; -import java.util.Objects; -import java.util.function.Consumer; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Quaterniond; -import org.joml.Quaternionf; -import org.joml.Vector3d; -import org.joml.Vector3f; - -public final class ExamplePhysicsUtils { - - public static final String DEFAULT_BLOCK_TYPE = - PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_BLOCK_TYPE; - private static final ComponentType TRANSFORM_TYPE = - TransformComponent.getComponentType(); - private static final ComponentType ATTACHMENT_TYPE = - PhysicsBodyAttachmentComponent.getComponentType(); - private static final ComponentType BODY_IDENTITY_TYPE = - PhysicsBodyIdentityComponent.getComponentType(); - private static final ComponentType BODY_SHAPE_TYPE = - PhysicsBodyShapeComponent.getComponentType(); - private static final ComponentType BODY_DYNAMICS_TYPE = - PhysicsBodyDynamicsComponent.getComponentType(); - private static final ComponentType BODY_MATERIAL_TYPE = - PhysicsBodyMaterialComponent.getComponentType(); - private static final ComponentType BODY_COLLISION_TYPE = - PhysicsBodyCollisionComponent.getComponentType(); - private static final ComponentType - BODY_KINEMATIC_TARGET_TYPE = PhysicsBodyKinematicTargetComponent.getComponentType(); - private static final ComponentType MODEL_TYPE = ModelComponent.getComponentType(); - private static final ComponentType HEAD_ROTATION_TYPE = HeadRotation.getComponentType(); - - private ExamplePhysicsUtils() { - } - - @Nullable - public static Vector3d playerPosition(@Nonnull CommandContext ctx, - @Nonnull Store store, - @Nonnull Ref ref) { - TransformComponent playerTransform = store.getComponent(ref, TRANSFORM_TYPE); - if (playerTransform == null) { - ctx.sender().sendMessage(Message.raw("Cannot determine player position.")); - return null; - } - return new Vector3d(playerTransform.getPosition()); - } - - @Nonnull - public static PhysicsWorldResource resource(@Nonnull Store store) { - return store.getResource(PhysicsWorldResource.getResourceType()); - } - - @Nullable - public static SpaceId spaceId(@Nonnull CommandContext ctx, - @Nonnull PhysicsWorldResource resource, - @Nonnull OptionalArg spaceArg) { - if (spaceArg.provided(ctx)) { - int rawSpaceId = spaceArg.get(ctx); - if (rawSpaceId <= 0) { - ctx.sender().sendMessage(Message.raw("Space id must be a positive integer.")); - return null; - } - SpaceId spaceId = new SpaceId(rawSpaceId); - if (!resource.hasSpace(spaceId)) { - ctx.sender().sendMessage(Message.raw("No physics space id=" + rawSpaceId + " exists.")); - return null; - } - return spaceId; - } - - SpaceId firstSpaceId = resource.getSpaceIds() - .stream() - .min(Comparator.comparingInt(SpaceId::value)) - .orElse(null); - if (firstSpaceId == null) { - ctx.sender().sendMessage(Message.raw("No physics space exists. Run " - + "`/impulse space create --backend=` before running Impulse example commands.")); - } - return firstSpaceId; - } - - @Nonnull - public static SpawnedBlockBody spawnBlockBody(@Nonnull Store store, - @Nonnull TimeResource time, - @Nonnull PhysicsWorldResource resource, - @Nonnull SpaceId spaceId, - @Nonnull Vector3d visualPosition, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull RigidBodySpawnSettings settings) { - return spawnBlockBody(store, time, resource, spaceId, visualPosition, DEFAULT_BLOCK_TYPE, - shape, mass, settings); - } - - @Nonnull - public static SpawnedBlockBody spawnBlockBody(@Nonnull Store store, - @Nonnull TimeResource time, - @Nonnull PhysicsWorldResource resource, - @Nonnull SpaceId spaceId, - @Nonnull Vector3d visualPosition, - @Nullable String blockType, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull RigidBodySpawnSettings settings) { - return spawnBlockBody(store, - time, - resource, - spaceId, - visualPosition, - blockType, - shape, - mass, - settings, - null); - } - - @Nonnull - public static SpawnedBlockBody spawnBlockBody(@Nonnull Store store, - @Nonnull TimeResource time, - @Nonnull PhysicsWorldResource resource, - @Nonnull SpaceId spaceId, - @Nonnull Vector3d visualPosition, - @Nullable String blockType, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull RigidBodySpawnSettings settings, - @Nullable Vector3f linearVelocity) { - PendingBlockBodyCapture pending = new PendingBlockBodyCapture(); - requireApplied(resource.submitCommands(0L, linearVelocity != null ? 2 : 1, commands -> - pending.set(recordBlockBodySpawn(commands, - spaceId, - visualPosition, - blockType, - shape, - mass, - settings, - linearVelocity))), "spawn attached block body"); - return attachRecordedBlockBody(store, time, pending.require()); - } - - @Nonnull - public static PendingBlockBody recordBlockBodySpawn(@Nonnull PhysicsCommandRecorder commands, - @Nonnull SpaceId spaceId, - @Nonnull Vector3d visualPosition, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull RigidBodySpawnSettings settings) { - return recordBlockBodySpawn(commands, - spaceId, - visualPosition, - DEFAULT_BLOCK_TYPE, - shape, - mass, - settings, - null); - } - - @Nonnull - public static PendingBlockBody recordBlockBodySpawn(@Nonnull PhysicsCommandRecorder commands, - @Nonnull SpaceId spaceId, - @Nonnull Vector3d visualPosition, - @Nullable String blockType, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull RigidBodySpawnSettings settings) { - return recordBlockBodySpawn(commands, - spaceId, - visualPosition, - blockType, - shape, - mass, - settings, - null); - } - - @Nonnull - public static PendingBlockBody recordBlockBodySpawn(@Nonnull PhysicsCommandRecorder commands, - @Nonnull SpaceId spaceId, - @Nonnull Vector3d visualPosition, - @Nullable String blockType, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull RigidBodySpawnSettings settings, - @Nullable Vector3f linearVelocity) { - Objects.requireNonNull(commands, "commands"); - Objects.requireNonNull(spaceId, "spaceId"); - Objects.requireNonNull(visualPosition, "visualPosition"); - Objects.requireNonNull(shape, "shape"); - Objects.requireNonNull(settings, "settings"); - return recordBlockBodySpawnAtResolvedPose(commands, - spaceId, - toVector3f(visualPosition), - visualPosition, - blockType, - shape, - mass, - settings, - linearVelocity); - } - - @Nonnull - public static PendingBlockBody recordBlockBodySpawnAtBodyCenter(@Nonnull PhysicsCommandRecorder commands, - @Nonnull SpaceId spaceId, - @Nonnull Vector3d bodyCenter, - @Nullable String blockType, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull RigidBodySpawnSettings settings) { - return recordBlockBodySpawnAtBodyCenter(commands, - spaceId, - bodyCenter, - blockType, - shape, - mass, - settings, - null); - } - - @Nonnull - public static PendingBlockBody recordBlockBodySpawnAtBodyCenter(@Nonnull PhysicsCommandRecorder commands, - @Nonnull SpaceId spaceId, - @Nonnull Vector3d bodyCenter, - @Nullable String blockType, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull RigidBodySpawnSettings settings, - @Nullable Vector3f linearVelocity) { - Objects.requireNonNull(bodyCenter, "bodyCenter"); - Objects.requireNonNull(shape, "shape"); - return recordBlockBodySpawnAtResolvedPose(commands, - spaceId, - new Vector3f((float) bodyCenter.x, (float) bodyCenter.y, (float) bodyCenter.z), - visualPositionFromBodyCenter(bodyCenter, shape), - blockType, - shape, - mass, - settings, - linearVelocity); - } - - @Nonnull - private static PendingBlockBody recordBlockBodySpawnAtResolvedPose(@Nonnull PhysicsCommandRecorder commands, - @Nonnull SpaceId spaceId, - @Nonnull Vector3f bodyCenter, - @Nonnull Vector3d visualPosition, - @Nullable String blockType, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull RigidBodySpawnSettings settings, - @Nullable Vector3f linearVelocity) { - Objects.requireNonNull(commands, "commands"); - Objects.requireNonNull(spaceId, "spaceId"); - Objects.requireNonNull(bodyCenter, "bodyCenter"); - Objects.requireNonNull(visualPosition, "visualPosition"); - Objects.requireNonNull(shape, "shape"); - Objects.requireNonNull(settings, "settings"); - RigidBodyKey bodyKey = RigidBodyKey.random(); - commands.spawnBody(bodyKey, spawn -> spawn - .space(spaceId) - .shape(shape) - .mass(mass) - .dynamic() - .position(bodyCenter.x, - bodyCenter.y, - bodyCenter.z) - .settings(settings) - .persistent()); - if (linearVelocity != null) { - commands.setBodyVelocity(bodyKey, - linearVelocity.x, - linearVelocity.y, - linearVelocity.z, - 0.0f, - 0.0f, - 0.0f, - true); - } - return new PendingBlockBody(bodyKey, - spaceId, - blockType, - (float) visualPosition.x, - (float) visualPosition.y, - (float) visualPosition.z, - mass > 0.0f); - } - - @Nonnull - public static SpawnedBlockBody attachRecordedBlockBody(@Nonnull Store store, - @Nonnull TimeResource time, - @Nonnull PendingBlockBody pending) { - Ref entity = spawnAttachedBlockEntity(store, - time, - pending.bodyKey(), - pending.spaceId(), - pending.blockType(), - new Vector3d(pending.positionX(), pending.positionY(), pending.positionZ()), - pending.controllable()); - assert entity != null; - return new SpawnedBlockBody(pending.bodyKey(), pending.spaceId(), entity); - } - - @Nonnull - public static SpawnedBlockBody[] spawnBlockBodies(@Nonnull Store store, - @Nonnull TimeResource time, - @Nonnull PhysicsWorldResource resource, - long serverTick, - @Nonnull SpaceId spaceId, - int expectedBodies, - @Nullable String blockType, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull Consumer recipe) { - return spawnBlockBodiesInternal(store, - time, - resource, - serverTick, - spaceId, - expectedBodies, - blockType, - shape, - mass, - settings, - recipe, - true).collectedBodies(); - } - - @Nonnull - public static BlockBodyBatchTiming spawnBlockBodiesMeasured(@Nonnull Store store, - @Nonnull TimeResource time, - @Nonnull PhysicsWorldResource resource, - long serverTick, - @Nonnull SpaceId spaceId, - int expectedBodies, - @Nullable String blockType, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull Consumer recipe) { - return spawnBlockBodiesInternal(store, - time, - resource, - serverTick, - spaceId, - expectedBodies, - blockType, - shape, - mass, - settings, - recipe, - false).timing(); - } - - @Nonnull - private static BlockBodyBatchResult spawnBlockBodiesInternal(@Nonnull Store store, - @Nonnull TimeResource time, - @Nonnull PhysicsWorldResource resource, - long serverTick, - @Nonnull SpaceId spaceId, - int expectedBodies, - @Nullable String blockType, - @Nonnull PhysicsShapeSpec shape, - float mass, - @Nonnull RigidBodySpawnSettings settings, - @Nonnull Consumer recipe, - boolean collectBodies) { - BlockBodyBatchRecorder batch = new BlockBodyBatchRecorder(expectedBodies); - Objects.requireNonNull(recipe, "recipe").accept(batch); - batch.seal(); - if (batch.isEmpty()) { - return new BlockBodyBatchResult(collectBodies ? new SpawnedBlockBody[0] : null, - 0, - 0L, - 0L); - } - - long commandStartNanos = System.nanoTime(); - requireApplied(resource.submitCommands(serverTick, 1, commands -> - commands.spawnBodies(batch.size(), - spaceId, - shape, - mass, - PhysicsBodyType.DYNAMIC, - settings, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.PERSISTENT, - spawns -> { - for (int i = 0; i < batch.size(); i++) { - spawns.body(batch.bodyKeyMostSignificantBits(i), - batch.bodyKeyLeastSignificantBits(i), - batch.positionX(i), - batch.positionY(i), - batch.positionZ(i)); - } - })), "spawn attached block bodies"); - long commandApplyNanos = System.nanoTime() - commandStartNanos; - - long entityAttachStartNanos = System.nanoTime(); - SpawnedBlockBody[] spawned = collectBodies ? new SpawnedBlockBody[batch.size()] : null; - for (int i = 0; i < batch.size(); i++) { - RigidBodyKey bodyKey = batch.bodyKey(i); - Ref entity = spawnAttachedBlockEntity(store, - time, - bodyKey, - spaceId, - blockType, - new Vector3d(batch.positionX(i), batch.positionY(i), batch.positionZ(i)), - mass > 0.0f); - if (spawned != null) { - assert entity != null; - spawned[i] = new SpawnedBlockBody(bodyKey, spaceId, entity); - } - } - long entityAttachNanos = System.nanoTime() - entityAttachStartNanos; - return new BlockBodyBatchResult(spawned, - batch.size(), - commandApplyNanos, - entityAttachNanos); - } - - public static void requireApplied(@Nonnull PhysicsCommandHandle handle, - @Nonnull String operation) { - handle.firstRejected() - .toCompletableFuture() - .join() - .ifPresent(result -> { - throw new IllegalStateException(operation + " command " - + result.commandSequence() + " rejected: " + result.message()); - }); - } - - @Nullable - public static Ref spawnPhysicsBodyBlockEntity(@Nonnull Store store, - @Nonnull TimeResource time, - @Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull Vector3d visualPosition, - @Nullable String blockType, - @Nonnull PhysicsBodyType bodyType, - float mass, - @Nullable PhysicsBodyKinematicTargetComponent kinematicTarget) { - Holder holder = physicsBodyBlockEntityHolder(time, - bodyKey, - spaceId, - visualPosition, - blockType, - bodyType, - mass, - 0.5f, - 0.2f, - kinematicTarget); - return store.addEntity(holder, AddReason.SPAWN); - } - - @Nonnull - public static Holder physicsBodyBlockEntityHolder(@Nonnull TimeResource time, - @Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull Vector3d visualPosition, - @Nullable String blockType, - @Nonnull PhysicsBodyType bodyType, - float mass, - float friction, - float restitution, - @Nullable PhysicsBodyKinematicTargetComponent kinematicTarget) { - Holder holder = blockEntityHolder(time, blockType, visualPosition); - holder.addComponent(BODY_IDENTITY_TYPE, - new PhysicsBodyIdentityComponent(bodyKey, - spaceId, - PhysicsBodyPersistenceMode.PERSISTENT)); - holder.addComponent(BODY_SHAPE_TYPE, - PhysicsBodyShapeComponent.box(0.5f, 0.5f, 0.5f)); - holder.addComponent(BODY_DYNAMICS_TYPE, - new PhysicsBodyDynamicsComponent(bodyType, mass, 0.0f, 0.0f)); - holder.addComponent(BODY_MATERIAL_TYPE, - new PhysicsBodyMaterialComponent(friction, restitution)); - holder.addComponent(BODY_COLLISION_TYPE, - new PhysicsBodyCollisionComponent(false, - PhysicsCollisionFilters.DYNAMIC_BODY, - PhysicsCollisionFilters.TERRAIN | PhysicsCollisionFilters.DYNAMIC_BODY)); - addControllableMarkerIfAvailable(holder, bodyType); - if (kinematicTarget != null) { - holder.addComponent(BODY_KINEMATIC_TARGET_TYPE, kinematicTarget); - } - return holder; - } - - static void addControllableMarkerIfAvailable(@Nonnull Holder holder, - @Nonnull PhysicsBodyType bodyType) { - Objects.requireNonNull(holder, "holder"); - Objects.requireNonNull(bodyType, "bodyType"); - if (bodyType == PhysicsBodyType.DYNAMIC && PhysicsControlSessions.isAvailable()) { - holder.addComponent(ImpulseControllableComponent.getComponentType(), - new ImpulseControllableComponent()); - } - } - - @Nonnull - public static PhysicsBodyKinematicTargetComponent kinematicTargetAt(@Nonnull Vector3d position) { - return new PhysicsBodyKinematicTargetComponent(toVector3f(position), - new Quaternionf(), - new Vector3f(), - new Vector3f(), - true, - false, - true); - } - - @Nullable - public static Ref spawnExternalBodyViewBlockEntity(@Nonnull Store store, - @Nonnull TimeResource time, - @Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull Vector3d visualPosition, - @Nullable String blockType) { - Holder holder = blockEntityHolder(time, blockType, visualPosition); - holder.addComponent(ATTACHMENT_TYPE, - PhysicsBodyAttachmentComponent.externalEntity(bodyKey, spaceId)); - return store.addEntity(holder, AddReason.SPAWN); - } - - @Nonnull - static Vector3d visualPositionFromBodyCenter(@Nonnull Vector3d bodyCenter, - @Nonnull PhysicsShapeSpec shape) { - return ExamplePhysicsOriginMath.visualPositionFromBodyCenter(bodyCenter, shape); - } - - @Nullable - public static Ref spawnAttachedBlockEntity(@Nonnull Store store, - @Nonnull TimeResource time, - @Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nullable String blockType, - @Nonnull Vector3d visualPosition, - boolean controllable) { - Holder holder = attachedBlockEntityHolder(time, - bodyKey, - spaceId, - blockType, - visualPosition, - controllable); - return store.addEntity(holder, AddReason.SPAWN); - } - - @Nonnull - public static Holder attachedBlockEntityHolder(@Nonnull TimeResource time, - @Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nullable String blockType, - @Nonnull Vector3d visualPosition, - boolean controllable) { - return attachedBlockEntityHolder(time, - bodyKey, - spaceId, - blockType, - visualPosition, - new Vector3f(), - controllable); - } - - @Nonnull - public static Holder attachedBlockEntityHolder(@Nonnull TimeResource time, - @Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nullable String blockType, - @Nonnull Vector3d visualPosition, - @Nonnull Vector3f localPositionOffset, - boolean controllable) { - return attachedBlockEntityHolder(time, - bodyKey, - spaceId, - blockType, - visualPosition, - localPositionOffset, - new Quaternionf(), - controllable); - } - - @Nonnull - public static Holder attachedBlockEntityHolder(@Nonnull TimeResource time, - @Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nullable String blockType, - @Nonnull Vector3d visualPosition, - @Nonnull Vector3f localPositionOffset, - float visualOriginOffsetY, - boolean controllable) { - return attachedBlockEntityHolder(time, - bodyKey, - spaceId, - blockType, - visualPosition, - localPositionOffset, - new Quaternionf(), - visualOriginOffsetY, - controllable); - } - - @Nonnull - public static Holder attachedBlockEntityHolder(@Nonnull TimeResource time, - @Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nullable String blockType, - @Nonnull Vector3d visualPosition, - @Nonnull Vector3f localPositionOffset, - @Nonnull Quaternionf localRotationOffset, - boolean controllable) { - return attachedBlockEntityHolder(time, - bodyKey, - spaceId, - blockType, - visualPosition, - localPositionOffset, - localRotationOffset, - Float.NaN, - controllable); - } - - @Nonnull - public static Holder attachedBlockEntityHolder(@Nonnull TimeResource time, - @Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nullable String blockType, - @Nonnull Vector3d visualPosition, - @Nonnull Vector3f localPositionOffset, - @Nonnull Quaternionf localRotationOffset, - float visualOriginOffsetY, - boolean controllable) { - Holder holder = blockEntityHolder(time, blockType, visualPosition); - holder.addComponent(ATTACHMENT_TYPE, - PhysicsBodyAttachmentComponent.impulseOwnedVisual(bodyKey, - spaceId, - localPositionOffset, - localRotationOffset, - visualOriginOffsetY)); - if (controllable && PhysicsControlSessions.isAvailable()) { - holder.addComponent(ImpulseControllableComponent.getComponentType(), - new ImpulseControllableComponent()); - } - return holder; - } - - @Nonnull - private static Holder blockEntityHolder(@Nonnull TimeResource time, - @Nullable String blockType, - @Nonnull Vector3d visualPosition) { - return ExampleBlockEntityVisuals.impulseOwnedBlockVisual(time, blockType, visualPosition); - } - - @Nonnull - public static String resolveBlockType(@Nullable String blockType) { - return ExampleBlockEntityVisuals.resolveBlockType(blockType); - } - - @Nonnull - static Vector3d eyePosition(@Nonnull Store store, - @Nonnull Ref ref, - @Nonnull TransformComponent transform) { - return new Vector3d(transform.getPosition()).add(0.0, eyeHeight(store, ref), 0.0); - } - - static double eyeHeight(@Nonnull Store store, @Nonnull Ref ref) { - ModelComponent modelComponent = store.getComponent(ref, MODEL_TYPE); - if (modelComponent == null) { - return 1.6; - } - - Model model = modelComponent.getModel(); - if (model == null) { - return 1.6; - } - return model.getEyeHeight(ref, store); - } - - @Nonnull - public static Vector3d lookDirection(@Nonnull Store store, - @Nonnull Ref ref, - @Nonnull TransformComponent transform) { - HeadRotation headRotation = store.getComponent(ref, HEAD_ROTATION_TYPE); - Rotation3f rotation = headRotation != null ? headRotation.getRotation() : transform.getRotation(); - Quaterniond quaternion = rotation.getQuaternion(new Quaterniond()); - Vector3d direction = new Vector3d(Vector3dUtil.FORWARD); - quaternion.transform(direction); - if (direction.lengthSquared() == 0.0) { - return new Vector3d(Vector3dUtil.FORWARD); - } - return direction.normalize(); - } - - public static int optionalInt(@Nonnull CommandContext ctx, - @Nonnull OptionalArg arg, - int defaultValue, - int min, - int max) { - int value = arg.provided(ctx) ? arg.get(ctx) : defaultValue; - if (value < min) { - return min; - } - return Math.min(value, max); - } - - static Vector3f toVector3f(@Nonnull Vector3d vector) { - return new Vector3f((float) vector.x, (float) vector.y, (float) vector.z); - } - - public record SpawnedBlockBody(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nonnull Ref entity) { - } - - public record BlockBodyBatchTiming(int count, - long commandApplyNanos, - long entityAttachNanos) { - - public BlockBodyBatchTiming { - count = Math.max(0, count); - commandApplyNanos = Math.max(0L, commandApplyNanos); - entityAttachNanos = Math.max(0L, entityAttachNanos); - } - } - - public record PendingBlockBody(@Nonnull RigidBodyKey bodyKey, - @Nonnull SpaceId spaceId, - @Nullable String blockType, - float positionX, - float positionY, - float positionZ, - boolean controllable) { - - public PendingBlockBody { - Objects.requireNonNull(bodyKey, "bodyKey"); - Objects.requireNonNull(spaceId, "spaceId"); - } - } - - private static final class PendingBlockBodyCapture { - - @Nullable - private PendingBlockBody body; - - private void set(@Nonnull PendingBlockBody body) { - this.body = Objects.requireNonNull(body, "body"); - } - - @Nonnull - private PendingBlockBody require() { - if (body == null) { - throw new IllegalStateException("Pending block body was not recorded"); - } - return body; - } - } - - public static final class BlockBodyBatchRecorder { - - private static final int POSITION_STRIDE = 3; - - private final long bodyKeyRunId = RigidBodyKey.random().mostSignificantBits(); - private long[] bodyKeyMostSignificantBits; - private long[] bodyKeyLeastSignificantBits; - private float[] positions; - private int size; - private boolean sealed; - - private BlockBodyBatchRecorder(int expectedBodies) { - int capacity = Math.max(1, expectedBodies); - bodyKeyMostSignificantBits = new long[capacity]; - bodyKeyLeastSignificantBits = new long[capacity]; - positions = new float[capacity * POSITION_STRIDE]; - } - - @Nonnull - public BlockBodyBatchRecorder addBody(float positionX, - float positionY, - float positionZ) { - return addBody(bodyKeyRunId, - size + 1L, - positionX, - positionY, - positionZ); - } - - @Nonnull - public BlockBodyBatchRecorder addBody(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ) { - Objects.requireNonNull(bodyKey, "bodyKey"); - return addBody(bodyKey.mostSignificantBits(), - bodyKey.leastSignificantBits(), - positionX, - positionY, - positionZ); - } - - @Nonnull - public RigidBodyKey body(float positionX, - float positionY, - float positionZ) { - long leastSignificantBits = size + 1L; - addBody(bodyKeyRunId, leastSignificantBits, positionX, positionY, positionZ); - return RigidBodyKey.of(bodyKeyRunId, leastSignificantBits); - } - - @Nonnull - public RigidBodyKey body(@Nonnull RigidBodyKey bodyKey, - float positionX, - float positionY, - float positionZ) { - addBody(bodyKey, positionX, positionY, positionZ); - return bodyKey; - } - - @Nonnull - private BlockBodyBatchRecorder addBody(long bodyKeyMostSignificantBits, - long bodyKeyLeastSignificantBits, - float positionX, - float positionY, - float positionZ) { - assertMutable(); - ensureCapacity(size + 1); - this.bodyKeyMostSignificantBits[size] = bodyKeyMostSignificantBits; - this.bodyKeyLeastSignificantBits[size] = bodyKeyLeastSignificantBits; - int positionOffset = size * POSITION_STRIDE; - positions[positionOffset] = positionX; - positions[positionOffset + 1] = positionY; - positions[positionOffset + 2] = positionZ; - size++; - return this; - } - - private void seal() { - sealed = true; - } - - private boolean isEmpty() { - return size == 0; - } - - private int size() { - return size; - } - - @Nonnull - private RigidBodyKey bodyKey(int index) { - checkIndex(index); - return RigidBodyKey.of(bodyKeyMostSignificantBits[index], - bodyKeyLeastSignificantBits[index]); - } - - private long bodyKeyMostSignificantBits(int index) { - checkIndex(index); - return bodyKeyMostSignificantBits[index]; - } - - private long bodyKeyLeastSignificantBits(int index) { - checkIndex(index); - return bodyKeyLeastSignificantBits[index]; - } - - private float positionX(int index) { - return position(index, 0); - } - - private float positionY(int index) { - return position(index, 1); - } - - private float positionZ(int index) { - return position(index, 2); - } - - private float position(int index, int slot) { - checkIndex(index); - return positions[index * POSITION_STRIDE + slot]; - } - - private void ensureCapacity(int required) { - if (required <= bodyKeyMostSignificantBits.length) { - return; - } - int nextCapacity = Math.max(required, - bodyKeyMostSignificantBits.length + (bodyKeyMostSignificantBits.length >> 1) + 1); - bodyKeyMostSignificantBits = Arrays.copyOf(bodyKeyMostSignificantBits, nextCapacity); - bodyKeyLeastSignificantBits = Arrays.copyOf(bodyKeyLeastSignificantBits, nextCapacity); - positions = Arrays.copyOf(positions, nextCapacity * POSITION_STRIDE); - } - - private void checkIndex(int index) { - if (index < 0 || index >= size) { - throw new IndexOutOfBoundsException(index); - } - } - - private void assertMutable() { - if (sealed) { - throw new IllegalStateException("Block body batch recorder is already sealed"); - } - } - } - - private record BlockBodyBatchResult(@Nullable SpawnedBlockBody[] bodies, - int count, - long commandApplyNanos, - long entityAttachNanos) { - - private BlockBodyBatchResult { - if (bodies != null && bodies.length != count) { - throw new IllegalArgumentException("Collected body count does not match batch count"); - } - count = Math.max(0, count); - commandApplyNanos = Math.max(0L, commandApplyNanos); - entityAttachNanos = Math.max(0L, entityAttachNanos); - } - - @Nonnull - private SpawnedBlockBody[] collectedBodies() { - if (bodies == null) { - throw new IllegalStateException("Block body batch did not collect body results"); - } - return bodies; - } - - @Nonnull - private BlockBodyBatchTiming timing() { - return new BlockBodyBatchTiming(count, - commandApplyNanos, - entityAttachNanos); - } - } -} diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ForcesCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ForcesCommand.java index fb0abd7e..31a1ebca 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ForcesCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ForcesCommand.java @@ -1,5 +1,6 @@ package dev.hytalemodding.impulse.examples.commands; +import com.hypixel.hytale.component.AddReason; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.Message; @@ -12,13 +13,17 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsRecipes; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.PhysicsCommandRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.examples.commands.ExamplePhysicsUtils.PendingBlockBody; +import dev.hytalemodding.impulse.core.plugin.components.BodyCommandComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodyEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils.CreatedBlockBody; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; import org.joml.Vector3d; @@ -42,99 +47,147 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - Vector3d playerPos = ExamplePhysicsUtils.playerPosition(ctx, store, ref); - if (playerPos == null) { - return CompletableFuture.completedFuture(null); - } + Vector3d playerPos = new Vector3d(playerRef.getTransform().getPosition()); - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, resource, spaceArg); + SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, world, spaceArg); if (spaceId == null) { return CompletableFuture.completedFuture(null); } TimeResource time = store.getResource(TimeResource.getResourceType()); + Store physicsStore = PhysicsThreading.store(world); + Ref spaceRef = ExamplePhysicsUtils.resolveSpaceRef(world, spaceId); + if (spaceRef == null) { + ctx.sender().sendMessage(Message.raw( + "Cannot spawn force demo because the target space is not bound in PhysicsStore.")); + return CompletableFuture.completedFuture(null); + } Vector3d origin = new Vector3d(playerPos).add(-2.0, 4.0, 4.0); Vector3d centralPosition = new Vector3d(origin); Vector3d offCenterPosition = new Vector3d(origin).add(2.0, 0.0, 0.0); Vector3d torquePosition = new Vector3d(origin).add(4.0, 0.0, 0.0); Vector3d forcePosition = new Vector3d(origin).add(6.0, 0.0, 0.0); - ForceDemoBodies bodies = new ForceDemoBodies(); - ExamplePhysicsUtils.requireApplied(resource.submitCommands(Math.max(0L, world.getTick()), 8, commands -> { - bodies.central = spawnBox(commands, spaceId, centralPosition); - commands.compose(PhysicsRecipes.applyImpulse(bodies.central.bodyKey(), - new Vector3f(4.0f, 2.0f, 0.0f))); - bodies.offCenter = spawnBox(commands, spaceId, offCenterPosition); - commands.applyBodyImpulse(bodies.offCenter.bodyKey(), 3.5f, 0.0f, 0.0f, 0.0f, 0.5f, 0.5f); - bodies.torque = spawnBox(commands, spaceId, torquePosition); - commands.applyBodyTorqueImpulse(bodies.torque.bodyKey(), 0.0f, 0.0f, 8.0f); - bodies.force = spawnBox(commands, spaceId, forcePosition); - commands.compose(PhysicsRecipes.applyForce(bodies.force.bodyKey(), - new Vector3f(30.0f, 0.0f, 0.0f))); - }), "apply force demo"); - PendingBlockBody central = bodies.requireCentral(); - PendingBlockBody offCenter = bodies.requireOffCenter(); - PendingBlockBody torque = bodies.requireTorque(); - PendingBlockBody force = bodies.requireForce(); + ForceDemoBodies bodies; + try { + bodies = createPhysicsStoreDemo(physicsStore, + spaceRef, + spaceId, + centralPosition, + offCenterPosition, + torquePosition, + forcePosition); + } catch (IllegalStateException exception) { + ctx.sender().sendMessage(Message.raw( + "Cannot spawn force demo because the target space is not bound in PhysicsStore.")); + return CompletableFuture.completedFuture(null); + } + CreatedBlockBody central = bodies.central(); + CreatedBlockBody offCenter = bodies.offCenter(); + CreatedBlockBody torque = bodies.torque(); + CreatedBlockBody force = bodies.force(); drawArrow(world, centralPosition, new Vector3d(2.0, 1.0, 0.0), DebugUtils.COLOR_GREEN); drawArrow(world, offCenterPosition, new Vector3d(2.0, 0.0, 0.0), DebugUtils.COLOR_YELLOW); drawArrow(world, torquePosition, new Vector3d(0.0, 0.0, 2.0), DebugUtils.COLOR_MAGENTA); drawArrow(world, forcePosition, new Vector3d(2.0, 0.0, 0.0), DebugUtils.COLOR_CYAN); - ExamplePhysicsUtils.attachRecordedBlockBody(store, time, central); - ExamplePhysicsUtils.attachRecordedBlockBody(store, time, offCenter); - ExamplePhysicsUtils.attachRecordedBlockBody(store, time, torque); - ExamplePhysicsUtils.attachRecordedBlockBody(store, time, force); + ExamplePhysicsUtils.attachBlockBody(store, time, central); + ExamplePhysicsUtils.attachBlockBody(store, time, offCenter); + ExamplePhysicsUtils.attachBlockBody(store, time, torque); + ExamplePhysicsUtils.attachBlockBody(store, time, force); ctx.sender().sendMessage(Message.raw( "Spawned force demo: central impulse, off-center impulse, torque, and force.")); return CompletableFuture.completedFuture(null); } - private static PendingBlockBody spawnBox(@Nonnull PhysicsCommandRecorder commandBuffer, + @Nonnull + private static ForceDemoBodies createPhysicsStoreDemo(@Nonnull Store physicsStore, + @Nonnull Ref spaceRef, @Nonnull SpaceId spaceId, - @Nonnull Vector3d position) { - return ExamplePhysicsUtils.recordBlockBodySpawn(commandBuffer, + @Nonnull Vector3d centralPosition, + @Nonnull Vector3d offCenterPosition, + @Nonnull Vector3d torquePosition, + @Nonnull Vector3d forcePosition) { + CreatedBlockBody central = spawnBox(physicsStore, + spaceRef, spaceId, - position, + centralPosition, + BodyCommandComponent.vector(BodyCommandComponent.Kind.IMPULSE, + 4.0f, + 2.0f, + 0.0f, + false, + 0.0f, + 0.0f, + 0.0f)); + CreatedBlockBody offCenter = spawnBox(physicsStore, + spaceRef, + spaceId, + offCenterPosition, + BodyCommandComponent.vector(BodyCommandComponent.Kind.IMPULSE, + 3.5f, + 0.0f, + 0.0f, + true, + 0.0f, + 0.5f, + 0.5f)); + CreatedBlockBody torque = spawnBox(physicsStore, + spaceRef, + spaceId, + torquePosition, + BodyCommandComponent.vector(BodyCommandComponent.Kind.TORQUE_IMPULSE, + 0.0f, + 0.0f, + 8.0f, + false, + 0.0f, + 0.0f, + 0.0f)); + CreatedBlockBody force = spawnBox(physicsStore, + spaceRef, + spaceId, + forcePosition, + BodyCommandComponent.vector(BodyCommandComponent.Kind.FORCE, + 30.0f, + 0.0f, + 0.0f, + false, + 0.0f, + 0.0f, + 0.0f)); + return new ForceDemoBodies(central, offCenter, torque, force); + } + + private static CreatedBlockBody spawnBox(@Nonnull Store physicsStore, + @Nonnull Ref spaceRef, + @Nonnull SpaceId spaceId, + @Nonnull Vector3d position, + @Nonnull BodyCommandComponent command) { + UUID bodyUuid = UUID.randomUUID(); + var bodyHolder = PhysicsBodyEntities.dynamicBodyHolder(spaceRef, + bodyUuid, + ExamplePhysicsUtils.toVector3f(position), PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f), 1.0f, - RigidBodySpawnSettings.material(0.5f, 0.25f)); + RigidBodySpawnSettings.material(0.5f, 0.25f), + null); + Ref bodyRef = physicsStore.addEntity(bodyHolder, AddReason.SPAWN); + assert bodyRef != null; + PhysicsBodies.appendCommand(physicsStore, bodyRef, command); + return new CreatedBlockBody(bodyUuid, + bodyRef, + spaceId, + ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE, + (float) position.x, + (float) position.y, + (float) position.z, + true); } - private static final class ForceDemoBodies { - - private PendingBlockBody central; - private PendingBlockBody offCenter; - private PendingBlockBody torque; - private PendingBlockBody force; - - @Nonnull - private PendingBlockBody requireCentral() { - return require(central, "central"); - } - - @Nonnull - private PendingBlockBody requireOffCenter() { - return require(offCenter, "off-center"); - } - - @Nonnull - private PendingBlockBody requireTorque() { - return require(torque, "torque"); - } - - @Nonnull - private PendingBlockBody requireForce() { - return require(force, "force"); - } - - @Nonnull - private static PendingBlockBody require(PendingBlockBody body, @Nonnull String name) { - if (body == null) { - throw new IllegalStateException("Missing " + name + " force demo body"); - } - return body; - } + private record ForceDemoBodies(@Nonnull CreatedBlockBody central, + @Nonnull CreatedBlockBody offCenter, + @Nonnull CreatedBlockBody torque, + @Nonnull CreatedBlockBody force) { } private static void drawArrow(@Nonnull World world, diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/GrabCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/GrabCommand.java index 587ee72e..d6815cef 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/GrabCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/GrabCommand.java @@ -1,37 +1,49 @@ package dev.hytalemodding.impulse.examples.commands; import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Holder; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Transform; import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.command.system.CommandContext; import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; import com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncPlayerCommand; -import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.TargetUtil; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; import dev.hytalemodding.impulse.api.PhysicsBodyType; +import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.modules.control.ImpulseControllableComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyRegistrationView; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.modules.control.PhysicsControlSessions; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RaycastAllQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.view.RaycastHitView; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RigidBodyStateQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.view.RigidBodyStateView; +import dev.hytalemodding.impulse.builtin.control.ImpulseControllableComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.builtin.control.PhysicsControlSessions; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollision; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodyEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsJointEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsAsync; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsSpaces; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsRaycasts; +import dev.hytalemodding.impulse.core.plugin.components.BodyCommandComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsEntityAttachments; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; +import dev.hytalemodding.impulse.core.plugin.components.JointType; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import dev.hytalemodding.impulse.core.plugin.physics.RaycastHitView; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; import org.joml.Quaternionf; import org.joml.Vector3d; import org.joml.Vector3f; @@ -41,10 +53,9 @@ public class GrabCommand extends AbstractAsyncPlayerCommand { private static final double RAY_LENGTH = 24.0; private static final float MIN_HOLD_DISTANCE = 4.0f; private static final Vector3f VIEW_OFFSET = new Vector3f(0.85f, -0.35f, 0.0f); - private static final ComponentType TRANSFORM_TYPE = - TransformComponent.getComponentType(); - private static final ComponentType ATTACHMENT_TYPE = - PhysicsBodyAttachmentComponent.getComponentType(); + private static final String PHYSICS_ENTITY_UNAVAILABLE_MESSAGE = + "Impulse PhysicsEntity integration is not available. " + + "Enable HytaleModding:ImpulsePhysicsEntity to grab entity-backed physics bodies."; private final OptionalArg spaceArg = this.withOptionalArg( "space", "Physics space id to target", @@ -61,65 +72,95 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - TransformComponent transform = store.getComponent(ref, TRANSFORM_TYPE); - if (transform == null) { - ctx.sender().sendMessage(Message.raw("Cannot determine player position.")); - return CompletableFuture.completedFuture(null); - } - if (!PhysicsControlSessions.isAvailable()) { ctx.sender().sendMessage(Message.raw( "Impulse control is disabled. Enable HytaleModding:ImpulseControl to use grab.")); return CompletableFuture.completedFuture(null); } + if (!PhysicsEntityAttachments.isAvailable()) { + ctx.sender().sendMessage(Message.raw(PHYSICS_ENTITY_UNAVAILABLE_MESSAGE)); + return CompletableFuture.completedFuture(null); + } ComponentType controllableType = ImpulseControllableComponent.getComponentType(); - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - SpaceId targetSpaceId = ExamplePhysicsUtils.spaceId(ctx, resource, spaceArg); + SpaceId targetSpaceId = ExamplePhysicsUtils.spaceId(ctx, world, spaceArg); if (targetSpaceId == null) { return CompletableFuture.completedFuture(null); } + Ref targetSpaceRef = ExamplePhysicsUtils.resolveSpaceRef(world, + targetSpaceId); + if (targetSpaceRef == null) { + ctx.sender().sendMessage(Message.raw("PhysicsStore space id=" + targetSpaceId.value() + + " is not bound yet.")); + return CompletableFuture.completedFuture(null); + } + Store physicsStore = PhysicsThreading.store(world); - Vector3d start = ExamplePhysicsUtils.eyePosition(store, ref, transform); - Vector3d direction = ExamplePhysicsUtils.lookDirection(store, ref, transform).mul(RAY_LENGTH); + Transform look = TargetUtil.getLook(ref, store); + Vector3d start = new Vector3d(look.getPosition()); + Vector3d direction = new Vector3d(look.getDirection()).mul(RAY_LENGTH); Vector3d end = new Vector3d(start).add(direction); - HitSelection selection = findControllableHit(resource, + return PhysicsAsync.acceptOnWorldThread(world, + PhysicsRaycasts.allAsync(world, + targetSpaceRef, + ExamplePhysicsUtils.toVector3f(start), + ExamplePhysicsUtils.toVector3f(end)), + hits -> finishGrab(ctx, + world, + store, + ref, + physicsStore, + targetSpaceId, + controllableType, + hits)); + } + + private static void finishGrab(@Nonnull CommandContext ctx, + @Nonnull World world, + @Nonnull Store store, + @Nonnull Ref ref, + @Nonnull Store physicsStore, + @Nonnull SpaceId targetSpaceId, + @Nonnull ComponentType controllableType, + @Nonnull List hits) { + if (!PhysicsEntityAttachments.isAvailable()) { + ctx.sender().sendMessage(Message.raw(PHYSICS_ENTITY_UNAVAILABLE_MESSAGE)); + return; + } + HitSelection selection = selectControllableHit(physicsStore, store, - targetSpaceId, + ref, controllableType, - start, - end); + hits); if (selection == null) { ctx.sender().sendMessage(Message.raw("No controllable physics body in sight.")); - return CompletableFuture.completedFuture(null); + return; } PhysicsControlSessions.releaseSession(store, ref); SpaceId selectedSpaceId = selection.spaceId() != null ? selection.spaceId() : targetSpaceId; - if (!resource.hasSpace(selectedSpaceId)) { + if (!PhysicsSpaces.hasSpace(physicsStore, selectedSpaceId)) { ctx.sender().sendMessage(Message.raw("Selected physics space no longer exists.")); - return CompletableFuture.completedFuture(null); + return; } - GrabPhysicsState physicsState = createGrabControl(resource, + GrabPhysicsState physicsState = createGrabControl(world, selectedSpaceId, - selection, - Math.max(0L, world.getTick())); + selection); if (physicsState == null) { ctx.sender().sendMessage(Message.raw("Selected physics body no longer exists.")); - return CompletableFuture.completedFuture(null); + return; } PhysicsControlSessions.startSession(store, ref, - selection.bodyKey(), - physicsState.anchorBodyKey(), - physicsState.controlJointKey(), + selection.bodyRef(), + physicsState.anchorBodyRef(), + physicsState.controlJointRef(), selection.attachment(), - selectedSpaceId, physicsState.originalBodyType(), Math.max(selection.distance(), MIN_HOLD_DISTANCE), VIEW_OFFSET, @@ -127,95 +168,125 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, ctx.sender().sendMessage(Message.raw("Grabbed physics body at distance " + selection.distance())); - return CompletableFuture.completedFuture(null); } @Nullable - private static GrabPhysicsState createGrabControl(@Nonnull PhysicsWorldResource resource, + private static GrabPhysicsState createGrabControl(@Nonnull World world, @Nonnull SpaceId selectedSpaceId, - @Nonnull HitSelection selection, - long serverTick) { - RigidBodyStateView selectedState = resource.query(new RigidBodyStateQuery(selection.bodyKey())) - .completion() - .toCompletableFuture() - .join() - .orElse(null); + @Nonnull HitSelection selection) { + PhysicsBodySnapshot selectedState = bodyState(world, selection.bodyRef()); if (selectedState == null) { return null; } + Ref spaceRef; + try { + spaceRef = ExamplePhysicsUtils.resolveSpaceRef(world, selectedSpaceId); + } catch (IllegalStateException exception) { + return null; + } + if (spaceRef == null) { + return null; + } Vector3f hitPoint = new Vector3f(selection.point()); - Vector3f bodyLocalHit = new Vector3f(hitPoint).sub(selectedState.pose().position()); - Quaternionf inverseBodyRotation = selectedState.pose().rotation(); + Vector3f bodyLocalHit = new Vector3f(hitPoint).sub(selectedState.position()); + Quaternionf inverseBodyRotation = selectedState.rotation(); inverseBodyRotation.invert().transform(bodyLocalHit); - RigidBodyKey anchorBodyKey = RigidBodyKey.random(); - JointKey controlJointKey = JointKey.random(); - boolean rejected = resource.submitCommands(serverTick, commands -> { - commands.spawnBody(anchorBodyKey, spawn -> spawn - .space(selectedSpaceId) - .sphere(0.08f) - .mass(1.0f) - .kinematic() - .position(hitPoint) - .settings(RigidBodySpawnSettings.defaults().withSensor(true).withCollisionFilter(1, 0)) - .temporary() - .runtimeOnly()); - commands.joint(controlJointKey, joint -> joint - .space(selectedSpaceId) - .bodies(anchorBodyKey, selection.bodyKey()) - .point(new Vector3f(), bodyLocalHit)); - commands.activateBody(selection.bodyKey()); - }) - .firstRejected() - .toCompletableFuture() - .join() - .isPresent(); - if (rejected) { + UUID anchorBodyUuid = UUID.randomUUID(); + UUID controlJointUuid = UUID.randomUUID(); + Ref selectedBodyRef = selection.bodyRef(); + if (!selectedBodyRef.isValid()) { + return null; + } + PhysicsBodies.appendCommand(selectedBodyRef.getStore(), + selectedBodyRef, + BodyCommandComponent.wake()); + try { + Ref anchorBodyRef = ExamplePhysicsUtils.addPhysicsStoreBody(world, + anchorBodyEntity(spaceRef, anchorBodyUuid, hitPoint)); + Ref controlJointRef = ExamplePhysicsUtils.addJoint(world, + controlJointUuid, + controlJoint(spaceRef, anchorBodyRef, selectedBodyRef, bodyLocalHit)); + return new GrabPhysicsState(selectedState.bodyType(), + anchorBodyRef, + controlJointRef, + hitPoint); + } catch (IllegalStateException exception) { return null; } - return new GrabPhysicsState(selectedState.bodyType(), anchorBodyKey, controlJointKey, hitPoint); + } + + @Nonnull + private static Holder anchorBodyEntity(@Nonnull Ref spaceRef, + @Nonnull UUID bodyUuid, + @Nonnull Vector3f hitPoint) { + return PhysicsBodyEntities.bodyHolder(spaceRef, + bodyUuid, + hitPoint, + PhysicsShapeSpec.sphere(0.08f), + PhysicsBodyType.KINEMATIC, + 1.0f, + RigidBodySpawnSettings.material(0.5f, 0.0f) + .withSensor(true) + .withCollisionFilter(PhysicsCollisionFilters.TERRAIN, 0), + null); + } + + @Nonnull + private static JointComponent controlJoint(@Nonnull Ref spaceRef, + @Nonnull Ref anchorBodyRef, + @Nonnull Ref bodyRef, + @Nonnull Vector3f bodyLocalHit) { + return PhysicsJointEntities.joint(spaceRef, + anchorBodyRef, + bodyRef, + JointType.POINT, + new Vector3f(), + bodyLocalHit, + new Vector3f()); } @Nullable - private static HitSelection findControllableHit(@Nonnull PhysicsWorldResource resource, + private static HitSelection selectControllableHit(@Nonnull Store physicsStore, @Nonnull Store store, - @Nonnull SpaceId spaceId, + @Nonnull Ref controllerRef, @Nonnull ComponentType controllableType, - @Nonnull Vector3d start, - @Nonnull Vector3d end) { - List hits = resource.query(new RaycastAllQuery(spaceId, - ExamplePhysicsUtils.toVector3f(start), - ExamplePhysicsUtils.toVector3f(end))) - .completion() - .toCompletableFuture() - .join(); + @Nonnull List hits) { List candidates = new ArrayList<>(hits.size()); for (RaycastHitView hit : hits) { - if (hit.bodyType() != PhysicsBodyType.DYNAMIC || hit.bodyKey() == null) { + if (hit.bodyType() != PhysicsBodyType.DYNAMIC + || hit.bodyRef() == null + || !hit.bodyRef().isValid()) { continue; } - PhysicsBodyRegistrationView registration = - resource.getBodyRegistrationView(hit.bodyKey()); - if (registration == null || registration.kind() != PhysicsBodyKind.BODY) { + SpaceId bodySpaceId = PhysicsBodies.spaceId(physicsStore, hit.bodyRef()); + if (bodySpaceId == null + || PhysicsChunkCollision.isChunkCollisionBody(physicsStore, hit.bodyRef())) { continue; } - candidates.add(new HitCandidate(registration.bodyKey(), - registration.spaceId(), + candidates.add(new HitCandidate(hit.bodyRef(), + bodySpaceId, hit.point(), hit.fraction(), hit.distance())); } HitSelection best = null; for (HitCandidate candidate : candidates) { + if (PhysicsControlSessions.isBodyControlled(candidate.bodyRef()) + && !PhysicsControlSessions.hasSessionForBody(store, + controllerRef, + candidate.bodyRef())) { + continue; + } AttachmentSelection attachments = - inspectGameplayAttachments(resource, store, controllableType, candidate.bodyKey()); + inspectGameplayAttachments(store, controllableType, candidate.bodyRef()); if (attachments.controllableAttachment() == null && attachments.hasGameplayAttachment()) { continue; } if (best == null || candidate.fraction() < best.fraction()) { - best = new HitSelection(candidate.bodyKey(), + best = new HitSelection(candidate.bodyRef(), attachments.controllableAttachment(), candidate.spaceId(), candidate.point(), @@ -226,16 +297,25 @@ private static HitSelection findControllableHit(@Nonnull PhysicsWorldResource re return best; } + @Nullable + private static PhysicsBodySnapshot bodyState(@Nonnull World world, + @Nonnull Ref bodyRef) { + Store store = PhysicsThreading.store(world); + return PhysicsBodies.snapshot(store, bodyRef); + } + @Nonnull - private static AttachmentSelection inspectGameplayAttachments(@Nonnull PhysicsWorldResource resource, - @Nonnull Store store, + private static AttachmentSelection inspectGameplayAttachments(@Nonnull Store store, @Nonnull ComponentType controllableType, - @Nonnull RigidBodyKey bodyKey) { + @Nonnull Ref bodyRef) { boolean hasGameplayAttachment = false; - for (Ref attachmentRef : resource.getBodyAttachments(bodyKey)) { - PhysicsBodyAttachmentComponent attachment = store.getComponent(attachmentRef, ATTACHMENT_TYPE); + PhysicsEntityAttachments.requireAvailable(); + ComponentType attachmentType = + BodyAttachmentComponent.getComponentType(); + for (Ref attachmentRef : PhysicsEntityAttachments.attachments(store, bodyRef)) { + BodyAttachmentComponent attachment = store.getComponent(attachmentRef, attachmentType); if (attachment == null - || attachment.getLifecycle() == PhysicsBodyAttachmentComponent.AttachmentLifecycle.GENERATED_PROXY) { + || attachment.getLifecycle() == BodyAttachmentComponent.AttachmentLifecycle.GENERATED_PROXY) { continue; } hasGameplayAttachment = true; @@ -248,7 +328,7 @@ private static AttachmentSelection inspectGameplayAttachments(@Nonnull PhysicsWo return new AttachmentSelection(null, hasGameplayAttachment); } - private record HitSelection(@Nonnull RigidBodyKey bodyKey, + private record HitSelection(@Nonnull Ref bodyRef, @Nullable Ref attachment, @Nullable SpaceId spaceId, @Nonnull Vector3f point, @@ -256,7 +336,7 @@ private record HitSelection(@Nonnull RigidBodyKey bodyKey, float distance) { } - private record HitCandidate(@Nonnull RigidBodyKey bodyKey, + private record HitCandidate(@Nonnull Ref bodyRef, @Nullable SpaceId spaceId, @Nonnull Vector3f point, float fraction, @@ -268,8 +348,8 @@ private record AttachmentSelection(@Nullable Ref controllableAttach } private record GrabPhysicsState(@Nonnull PhysicsBodyType originalBodyType, - @Nonnull RigidBodyKey anchorBodyKey, - @Nonnull JointKey controlJointKey, + @Nonnull Ref anchorBodyRef, + @Nonnull Ref controlJointRef, @Nonnull Vector3f hitPoint) { } } diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ImpulseCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ImpulseCommand.java index 7e2605e5..63cb776f 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ImpulseCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ImpulseCommand.java @@ -7,18 +7,22 @@ public class ImpulseCommand extends AbstractCommandCollection { public ImpulseCommand() { super("impulse-examples", "Impulse example and stress test commands"); - addSubCommand(new EcsCommand()); addSubCommand(new DropCommand()); addSubCommand(new ShapesCommand()); addSubCommand(new MaterialsCommand()); addSubCommand(new ForcesCommand()); addSubCommand(new JointsCommand()); + addSubCommand(new DirectionalPendulumsCommand()); addSubCommand(new RaycastCommand()); + addSubCommand(new PhysicsStoreExampleCommands.BumperCommand()); + addSubCommand(new PhysicsStoreExampleCommands.PlatformCommand()); + addSubCommand(new PhysicsStoreExampleCommands.PickupCommand()); + addSubCommand(new PhysicsStoreExampleCommands.ExplosiveCommand()); addSubCommand(new EventsCommand()); addSubCommand(new GrabCommand()); addSubCommand(new ReleaseCommand()); addSubCommand(new PersistenceCommand()); - addSubCommand(new WorldCollisionCommand()); + addSubCommand(new PhysicsChunkExampleCommand()); addSubCommand(new StressCommand()); } } diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/JointsCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/JointsCommand.java index 84eabf8e..313a06d3 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/JointsCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/JointsCommand.java @@ -1,5 +1,6 @@ package dev.hytalemodding.impulse.examples.commands; +import com.hypixel.hytale.component.AddReason; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.Message; @@ -11,18 +12,24 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.PhysicsCommandRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.examples.commands.ExamplePhysicsUtils.PendingBlockBody; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodyEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsJointEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointType; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils.CreatedBlockBody; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.joml.Vector3d; import org.joml.Vector3f; @@ -47,29 +54,35 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - Vector3d playerPos = ExamplePhysicsUtils.playerPosition(ctx, store, ref); - if (playerPos == null) { - return CompletableFuture.completedFuture(null); - } + Vector3d playerPos = new Vector3d(playerRef.getTransform().getPosition()); - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, resource, spaceArg); + SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, world, spaceArg); if (spaceId == null) { return CompletableFuture.completedFuture(null); } TimeResource time = store.getResource(TimeResource.getResourceType()); + Store physicsStore = PhysicsThreading.store(world); + Ref spaceRef = ExamplePhysicsUtils.resolveSpaceRef(world, spaceId); + if (spaceRef == null) { + ctx.sender().sendMessage(Message.raw( + "Cannot spawn joint demo because the target space is not bound in PhysicsStore.")); + return CompletableFuture.completedFuture(null); + } Vector3d origin = new Vector3d(playerPos).add(-5.0, 5.0, 5.0); - List pendingBodies = new ArrayList<>(10); - ExamplePhysicsUtils.requireApplied(resource.submitCommands(Math.max(0L, world.getTick()), 16, commands -> { - createFixed(pendingBodies, commands, spaceId, new Vector3d(origin)); - createPoint(pendingBodies, commands, spaceId, new Vector3d(origin).add(2.5, 0.0, 0.0)); - createHinge(pendingBodies, commands, spaceId, new Vector3d(origin).add(5.0, 0.0, 0.0)); - createSlider(pendingBodies, commands, spaceId, new Vector3d(origin).add(7.5, 0.0, 0.0)); - createSpring(pendingBodies, commands, spaceId, new Vector3d(origin).add(10.0, 0.0, 0.0)); - }), "create joint demo"); - for (PendingBlockBody pending : pendingBodies) { - ExamplePhysicsUtils.attachRecordedBlockBody(store, time, pending); + List createdBodies; + try { + createdBodies = createPhysicsStoreDemo(physicsStore, + spaceRef, + spaceId, + new Vector3d(origin)); + } catch (IllegalStateException exception) { + ctx.sender().sendMessage(Message.raw( + "Cannot spawn joint demo because the target space is not bound in PhysicsStore.")); + return CompletableFuture.completedFuture(null); + } + for (CreatedBlockBody created : createdBodies) { + ExamplePhysicsUtils.attachBlockBody(store, time, created); } ctx.sender().sendMessage(Message.raw( @@ -77,100 +90,217 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, return CompletableFuture.completedFuture(null); } - private static void createFixed(@Nonnull List pendingBodies, - @Nonnull PhysicsCommandRecorder commands, + @Nonnull + private static List createPhysicsStoreDemo(@Nonnull Store physicsStore, + @Nonnull Ref spaceRef, @Nonnull SpaceId spaceId, @Nonnull Vector3d origin) { - RigidBodyKey anchorKey = spawnBox(pendingBodies, commands, spaceId, origin, 0.0f); - RigidBodyKey childKey = spawnBox(pendingBodies, commands, spaceId, - new Vector3d(origin).add(0.0, -TOUCHING_SPACING, 0.0), 1.0f); - commands.joint(JointKey.random(), joint -> joint - .space(spaceId) - .bodies(anchorKey, childKey) - .fixed(new Vector3f(0.0f, -HALF_SIZE, 0.0f), - new Vector3f(0.0f, HALF_SIZE, 0.0f))); + List createdBodies = new ArrayList<>(10); + createFixed(createdBodies, physicsStore, spaceRef, spaceId, new Vector3d(origin)); + createPoint(createdBodies, + physicsStore, + spaceRef, + spaceId, + new Vector3d(origin).add(2.5, 0.0, 0.0)); + createHinge(createdBodies, + physicsStore, + spaceRef, + spaceId, + new Vector3d(origin).add(5.0, 0.0, 0.0)); + createSlider(createdBodies, + physicsStore, + spaceRef, + spaceId, + new Vector3d(origin).add(7.5, 0.0, 0.0)); + createSpring(createdBodies, + physicsStore, + spaceRef, + spaceId, + new Vector3d(origin).add(10.0, 0.0, 0.0)); + return createdBodies; } - private static void createPoint(@Nonnull List pendingBodies, - @Nonnull PhysicsCommandRecorder commands, + private static void createFixed(@Nonnull List createdBodies, + @Nonnull Store physicsStore, + @Nonnull Ref spaceRef, @Nonnull SpaceId spaceId, @Nonnull Vector3d origin) { - RigidBodyKey anchorKey = spawnBox(pendingBodies, commands, spaceId, origin, 0.0f); - RigidBodyKey bobKey = spawnBox(pendingBodies, commands, spaceId, + CreatedBlockBody anchor = spawnBox(createdBodies, physicsStore, spaceRef, spaceId, origin, 0.0f); + CreatedBlockBody child = spawnBox(createdBodies, physicsStore, spaceRef, spaceId, new Vector3d(origin).add(0.0, -TOUCHING_SPACING, 0.0), 1.0f); - commands.setBodyVelocity(bobKey, 1.5f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, true); - commands.joint(JointKey.random(), joint -> joint - .space(spaceId) - .bodies(anchorKey, bobKey) - .point(new Vector3f(0.0f, -HALF_SIZE, 0.0f), - new Vector3f(0.0f, HALF_SIZE, 0.0f))); + JointComponent joint = joint(spaceRef, + anchor, + child, + JointType.FIXED, + new Vector3f(0.0f, -HALF_SIZE, 0.0f), + new Vector3f(0.0f, HALF_SIZE, 0.0f), + new Vector3f()); + Ref jointRef = physicsStore.addEntity(PhysicsEntities.jointHolder(physicsStore, + UUID.randomUUID(), + joint), AddReason.SPAWN); + assert jointRef != null; + } + + private static void createPoint(@Nonnull List createdBodies, + @Nonnull Store physicsStore, + @Nonnull Ref spaceRef, + @Nonnull SpaceId spaceId, + @Nonnull Vector3d origin) { + CreatedBlockBody anchor = spawnBox(createdBodies, physicsStore, spaceRef, spaceId, origin, 0.0f); + CreatedBlockBody bob = spawnBox(createdBodies, + physicsStore, + spaceRef, + spaceId, + new Vector3d(origin).add(0.0, -TOUCHING_SPACING, 0.0), + 1.0f, + new Vector3f(1.5f, 0.0f, 0.0f)); + JointComponent joint = joint(spaceRef, + anchor, + bob, + JointType.POINT, + new Vector3f(0.0f, -HALF_SIZE, 0.0f), + new Vector3f(0.0f, HALF_SIZE, 0.0f), + new Vector3f()); + Ref jointRef = physicsStore.addEntity(PhysicsEntities.jointHolder(physicsStore, + UUID.randomUUID(), + joint), AddReason.SPAWN); + assert jointRef != null; } - private static void createHinge(@Nonnull List pendingBodies, - @Nonnull PhysicsCommandRecorder commands, + private static void createHinge(@Nonnull List createdBodies, + @Nonnull Store physicsStore, + @Nonnull Ref spaceRef, @Nonnull SpaceId spaceId, @Nonnull Vector3d origin) { - RigidBodyKey anchorKey = spawnBox(pendingBodies, commands, spaceId, origin, 0.0f); - RigidBodyKey armKey = spawnBox(pendingBodies, commands, spaceId, + CreatedBlockBody anchor = spawnBox(createdBodies, physicsStore, spaceRef, spaceId, origin, 0.0f); + CreatedBlockBody arm = spawnBox(createdBodies, physicsStore, spaceRef, spaceId, new Vector3d(origin).add(0.0, -TOUCHING_SPACING, 0.0), 1.0f); - commands.joint(JointKey.random(), joint -> joint - .space(spaceId) - .bodies(anchorKey, armKey) - .hinge(new Vector3f(0.0f, -HALF_SIZE, 0.0f), - new Vector3f(0.0f, HALF_SIZE, 0.0f), - new Vector3f(0.0f, 0.0f, 1.0f)) - .limits(-1.2f, 1.2f) - .motor(1.5f, 3.0f)); + JointComponent joint = joint(spaceRef, + anchor, + arm, + JointType.HINGE, + new Vector3f(0.0f, -HALF_SIZE, 0.0f), + new Vector3f(0.0f, HALF_SIZE, 0.0f), + new Vector3f(0.0f, 0.0f, 1.0f)); + joint.setLowerLimit(-1.2f); + joint.setUpperLimit(1.2f); + joint.setMotorEnabled(true); + joint.setMotorTargetVelocity(1.5f); + joint.setMotorMaxForce(3.0f); + Ref jointRef = physicsStore.addEntity(PhysicsEntities.jointHolder(physicsStore, + UUID.randomUUID(), + joint), AddReason.SPAWN); + assert jointRef != null; } - private static void createSlider(@Nonnull List pendingBodies, - @Nonnull PhysicsCommandRecorder commands, + private static void createSlider(@Nonnull List createdBodies, + @Nonnull Store physicsStore, + @Nonnull Ref spaceRef, @Nonnull SpaceId spaceId, @Nonnull Vector3d origin) { - RigidBodyKey anchorKey = spawnBox(pendingBodies, commands, spaceId, origin, 0.0f); - RigidBodyKey blockKey = spawnBox(pendingBodies, commands, spaceId, + CreatedBlockBody anchor = spawnBox(createdBodies, physicsStore, spaceRef, spaceId, origin, 0.0f); + CreatedBlockBody block = spawnBox(createdBodies, physicsStore, spaceRef, spaceId, new Vector3d(origin).add(TOUCHING_SPACING, 0.0, 0.0), 1.0f); - commands.joint(JointKey.random(), joint -> joint - .space(spaceId) - .bodies(anchorKey, blockKey) - .slider(new Vector3f(HALF_SIZE, 0.0f, 0.0f), - new Vector3f(-HALF_SIZE, 0.0f, 0.0f), - new Vector3f(1.0f, 0.0f, 0.0f)) - .limits(-1.0f, 1.0f) - .motor(1.0f, 4.0f)); + JointComponent joint = joint(spaceRef, + anchor, + block, + JointType.SLIDER, + new Vector3f(HALF_SIZE, 0.0f, 0.0f), + new Vector3f(-HALF_SIZE, 0.0f, 0.0f), + new Vector3f(1.0f, 0.0f, 0.0f)); + joint.setLowerLimit(-1.0f); + joint.setUpperLimit(1.0f); + joint.setMotorEnabled(true); + joint.setMotorTargetVelocity(1.0f); + joint.setMotorMaxForce(4.0f); + Ref jointRef = physicsStore.addEntity(PhysicsEntities.jointHolder(physicsStore, + UUID.randomUUID(), + joint), AddReason.SPAWN); + assert jointRef != null; } - private static void createSpring(@Nonnull List pendingBodies, - @Nonnull PhysicsCommandRecorder commands, + private static void createSpring(@Nonnull List createdBodies, + @Nonnull Store physicsStore, + @Nonnull Ref spaceRef, @Nonnull SpaceId spaceId, @Nonnull Vector3d origin) { - RigidBodyKey anchorKey = spawnBox(pendingBodies, commands, spaceId, origin, 0.0f); - RigidBodyKey bobKey = spawnBox(pendingBodies, commands, spaceId, + CreatedBlockBody anchor = spawnBox(createdBodies, physicsStore, spaceRef, spaceId, origin, 0.0f); + CreatedBlockBody bob = spawnBox(createdBodies, + physicsStore, + spaceRef, + spaceId, new Vector3d(origin).add(0.0, -(TOUCHING_SPACING + SPRING_REST_LENGTH), 0.0), - 1.0f); - commands.setBodyVelocity(bobKey, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, true); - commands.joint(JointKey.random(), joint -> joint - .space(spaceId) - .bodies(anchorKey, bobKey) - .spring(new Vector3f(0.0f, -HALF_SIZE, 0.0f), - new Vector3f(0.0f, HALF_SIZE, 0.0f), - SPRING_REST_LENGTH, - 20.0f, - 2.0f)); + 1.0f, + new Vector3f(1.0f, 0.0f, 0.0f)); + JointComponent joint = joint(spaceRef, + anchor, + bob, + JointType.SPRING, + new Vector3f(0.0f, -HALF_SIZE, 0.0f), + new Vector3f(0.0f, HALF_SIZE, 0.0f), + new Vector3f()); + joint.setSpringRestLength(SPRING_REST_LENGTH); + joint.setSpringStiffness(20.0f); + joint.setSpringDamping(2.0f); + Ref jointRef = physicsStore.addEntity(PhysicsEntities.jointHolder(physicsStore, + UUID.randomUUID(), + joint), AddReason.SPAWN); + assert jointRef != null; } - private static RigidBodyKey spawnBox(@Nonnull List pendingBodies, - @Nonnull PhysicsCommandRecorder commands, + private static CreatedBlockBody spawnBox(@Nonnull List createdBodies, + @Nonnull Store physicsStore, + @Nonnull Ref spaceRef, @Nonnull SpaceId spaceId, @Nonnull Vector3d position, float mass) { - PendingBlockBody pending = ExamplePhysicsUtils.recordBlockBodySpawn(commands, - spaceId, - position, + return spawnBox(createdBodies, physicsStore, spaceRef, spaceId, position, mass, null); + } + + private static CreatedBlockBody spawnBox(@Nonnull List createdBodies, + @Nonnull Store physicsStore, + @Nonnull Ref spaceRef, + @Nonnull SpaceId spaceId, + @Nonnull Vector3d position, + float mass, + @Nullable Vector3f linearVelocity) { + UUID bodyUuid = UUID.randomUUID(); + var bodyHolder = PhysicsBodyEntities.dynamicBodyHolder(spaceRef, + bodyUuid, + ExamplePhysicsUtils.toVector3f(position), PhysicsShapeSpec.box(HALF_SIZE, HALF_SIZE, HALF_SIZE), mass, - RigidBodySpawnSettings.material(0.6f, 0.15f)); - pendingBodies.add(pending); - return pending.bodyKey(); + RigidBodySpawnSettings.material(0.6f, 0.15f), + linearVelocity); + Ref bodyRef = physicsStore.addEntity(bodyHolder, AddReason.SPAWN); + assert bodyRef != null; + CreatedBlockBody created = new CreatedBlockBody(bodyUuid, + bodyRef, + spaceId, + ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE, + (float) position.x, + (float) position.y, + (float) position.z, + mass > 0.0f); + createdBodies.add(created); + return created; } + + @Nonnull + private static JointComponent joint(@Nonnull Ref spaceRef, + @Nonnull CreatedBlockBody bodyA, + @Nonnull CreatedBlockBody bodyB, + @Nonnull JointType type, + @Nonnull Vector3f anchorA, + @Nonnull Vector3f anchorB, + @Nonnull Vector3f axis) { + return PhysicsJointEntities.joint(spaceRef, + bodyA.bodyRef(), + bodyB.bodyRef(), + type, + anchorA, + anchorB, + axis); + } + } diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/MaterialsCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/MaterialsCommand.java index 0b9dc8f9..031ac5e5 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/MaterialsCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/MaterialsCommand.java @@ -1,5 +1,6 @@ package dev.hytalemodding.impulse.examples.commands; +import com.hypixel.hytale.component.AddReason; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.Message; @@ -11,12 +12,16 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodyEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; +import org.joml.Quaternionf; import org.joml.Vector3d; import org.joml.Vector3f; @@ -38,25 +43,43 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - Vector3d playerPos = ExamplePhysicsUtils.playerPosition(ctx, store, ref); - if (playerPos == null) { - return CompletableFuture.completedFuture(null); - } + Vector3d playerPos = new Vector3d(playerRef.getTransform().getPosition()); - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, resource, spaceArg); - if (spaceId == null) { + ExamplePhysicsUtils.SpaceSelection space = ExamplePhysicsUtils.spaceSelection(ctx, + world, + spaceArg); + if (space == null) { return CompletableFuture.completedFuture(null); } TimeResource time = store.getResource(TimeResource.getResourceType()); + Store physicsStore = PhysicsThreading.store(world); + PhysicsThreading.requireWorldThread(physicsStore, + "spawn material example PhysicsStore body entities"); Vector3d origin = new Vector3d(playerPos).add(-3.0, 5.0, 4.0); - spawnSphere(store, time, resource, spaceId, new Vector3d(origin), 0.05f, 0.9f, 3.0f); - spawnSphere(store, time, resource, spaceId, new Vector3d(origin).add(2.0, 0.0, 0.0), + spawnSphere(store, + physicsStore, + time, + space.spaceRef(), + new Vector3d(origin), + 0.05f, 0.9f, 3.0f); + spawnSphere(store, + physicsStore, + time, + space.spaceRef(), + new Vector3d(origin).add(2.0, 0.0, 0.0), 0.95f, 0.9f, 3.0f); - spawnSphere(store, time, resource, spaceId, new Vector3d(origin).add(4.0, 0.0, 0.0), + spawnSphere(store, + physicsStore, + time, + space.spaceRef(), + new Vector3d(origin).add(4.0, 0.0, 0.0), 0.5f, 0.0f, 2.0f); - spawnSphere(store, time, resource, spaceId, new Vector3d(origin).add(6.0, 0.0, 0.0), + spawnSphere(store, + physicsStore, + time, + space.spaceRef(), + new Vector3d(origin).add(6.0, 0.0, 0.0), 0.5f, 0.95f, 2.0f); ctx.sender().sendMessage(Message.raw( @@ -65,22 +88,33 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, } private static void spawnSphere(@Nonnull Store store, + @Nonnull Store physicsStore, @Nonnull TimeResource time, - @Nonnull PhysicsWorldResource resource, - @Nonnull SpaceId spaceId, + @Nonnull Ref spaceRef, @Nonnull Vector3d position, float restitution, float friction, float speed) { - ExamplePhysicsUtils.spawnBlockBody(store, - time, - resource, - spaceId, - position, - ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE, + UUID bodyUuid = UUID.randomUUID(); + var bodyHolder = PhysicsBodyEntities.dynamicBodyHolder(spaceRef, + bodyUuid, + ExamplePhysicsUtils.toVector3f(position), PhysicsShapeSpec.sphere(0.5f), 1.0f, RigidBodySpawnSettings.material(friction, restitution), new Vector3f(speed, 0.0f, 0.0f)); + Ref bodyRef = physicsStore.addEntity(bodyHolder, AddReason.SPAWN); + assert bodyRef != null; + store.addEntity(ExamplePhysicsUtils.attachedPhysicsBlockEntityHolder( + time, + bodyRef, + bodyUuid, + ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE, + position, + new Vector3f(), + new Quaternionf(), + Float.NaN, + true), + AddReason.SPAWN); } } diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/PersistenceCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/PersistenceCommand.java index fb8fd169..62ecec99 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/PersistenceCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/PersistenceCommand.java @@ -3,16 +3,16 @@ import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.command.system.CommandContext; -import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; -import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection; -import com.hypixel.hytale.server.core.command.system.basecommands.AbstractWorldCommand; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncWorldCommand; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; import dev.hytalemodding.impulse.core.plugin.persistence.PhysicsPersistence; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsAsync; import dev.hytalemodding.impulse.core.plugin.persistence.PhysicsPersistence.RestoreRequestResult; import dev.hytalemodding.impulse.core.plugin.persistence.PhysicsPersistence.SaveResult; import dev.hytalemodding.impulse.core.plugin.persistence.PhysicsPersistence.Status; +import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; /** @@ -27,20 +27,34 @@ public PersistenceCommand() { addSubCommand(new StatusCommand()); } - private static final class SaveCommand extends AbstractWorldCommand { + private static final class SaveCommand extends AbstractAsyncWorldCommand { private SaveCommand() { - super("save", "Synchronize current runtime physics into Hytale world persistence", false); + super("save", "Report authoritative PhysicsStore persistence capture status", false); } + @Nonnull @Override - protected void execute(@Nonnull CommandContext ctx, + protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, + @Nonnull World world) { + Store store = world.getEntityStore().getStore(); + return PhysicsAsync.acceptOnWorldThread(world, + PhysicsPersistence.saveRuntimeSnapshotAsync(store), + result -> sendSaveResult(ctx, world, result)); + } + + private static void sendSaveResult(@Nonnull CommandContext ctx, @Nonnull World world, - @Nonnull Store store) { - SaveResult result = PhysicsPersistence.saveRuntimeSnapshot(store); + @Nonnull SaveResult result) { if (!result.synced()) { - ctx.sendMessage(Message.raw("Cannot save Impulse world persistence while " - + result.skippedReason() + ".")); + ctx.sendMessage(Message.raw("Manual Impulse persistence save is disabled in " + + "authoritative PhysicsStore mode; PhysicsStore holders are saved " + + "by the world save hook. reason=" + result.skippedReason() + + ", stored schema=" + result.schemaVersion() + + ", spaces=" + result.spaces() + + ", persistentBodies=" + result.bodies() + + ", joints=" + result.joints() + + ".")); return; } @@ -53,42 +67,42 @@ protected void execute(@Nonnull CommandContext ctx, } } - private static final class LoadCommand extends AbstractWorldCommand { - - private final OptionalArg confirmArg = withOptionalArg( - "confirm", - "Required: true, because restore resets runtime physics before hydration rebuilds it", - ArgTypes.STRING); + private static final class LoadCommand extends AbstractAsyncWorldCommand { private LoadCommand() { - super("load", "Restore runtime physics from Hytale world persistence", false); + super("load", "Report authoritative PhysicsStore persistence restore status", false); } + @Nonnull @Override - protected void execute(@Nonnull CommandContext ctx, + protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, + @Nonnull World world) { + Store store = world.getEntityStore().getStore(); + return PhysicsAsync.acceptOnWorldThread(world, + PhysicsPersistence.requestRuntimeRestoreAsync(store), + result -> sendLoadResult(ctx, world, result)); + } + + private static void sendLoadResult(@Nonnull CommandContext ctx, @Nonnull World world, - @Nonnull Store store) { - Status status = PhysicsPersistence.status(store); + @Nonnull RestoreRequestResult result) { + Status status = result.status(); if (status.restoreState() == PhysicsPersistence.RestoreState.PENDING_SPACES || status.restoreState() == PhysicsPersistence.RestoreState.PENDING_BODIES_AND_JOINTS) { ctx.sendMessage(Message.raw("Impulse persistence restore is already pending.")); return; } - if (!confirmRestore(ctx, world, status)) { - return; - } - - RestoreRequestResult result = PhysicsPersistence.requestRuntimeRestore(store); if (!result.queued()) { - if ("schema-mismatch".equals(result.skippedReason())) { - ctx.sendMessage(Message.raw("Cannot load Impulse world persistence schema " - + status.schemaVersion() + "; expected schema " - + PhysicsPersistence.CURRENT_SCHEMA_VERSION + ".")); - } else { - ctx.sendMessage(Message.raw("Cannot queue Impulse persistence restore: " - + result.skippedReason() + ".")); - } + ctx.sendMessage(Message.raw("Manual Impulse persistence restore is disabled in " + + "authoritative PhysicsStore mode; PhysicsStore restores automatically from " + + "holder storage during world startup. reason=" + + result.skippedReason() + + ", stored schema=" + status.schemaVersion() + + ", spaces=" + status.storedSpaces() + + ", persistentBodies=" + status.storedBodies() + + ", joints=" + status.storedJoints() + + ".")); return; } @@ -100,45 +114,33 @@ protected void execute(@Nonnull CommandContext ctx, + ", joints=" + status.storedJoints() + ". Runtime physics will reset and hydrate on the next tick.")); } - - private boolean confirmRestore(@Nonnull CommandContext ctx, - @Nonnull World world, - @Nonnull Status status) { - if (confirmArg.provided(ctx) && "true".equalsIgnoreCase(confirmArg.get(ctx).trim())) { - return true; - } - - ctx.sendMessage(Message.raw("Loading Impulse world persistence for " + world.getName() - + " resets runtime spaces, bodies, joints, generated visual proxies, and control sessions, " - + "then rebuilds from the stored world resource (schema=" + status.schemaVersion() - + ", spaces=" + status.storedSpaces() - + ", persistentBodies=" + status.storedBodies() - + ", joints=" + status.storedJoints() - + "). Re-run with --confirm=true to continue.")); - return false; - } } - private static final class StatusCommand extends AbstractWorldCommand { + private static final class StatusCommand extends AbstractAsyncWorldCommand { private StatusCommand() { super("status", "Show runtime and stored Impulse persistence counts", false); } + @Nonnull @Override - protected void execute(@Nonnull CommandContext ctx, - @Nonnull World world, - @Nonnull Store store) { - Status status = PhysicsPersistence.status(store); + protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, + @Nonnull World world) { + Store store = world.getEntityStore().getStore(); + return PhysicsAsync.acceptOnWorldThread(world, + PhysicsPersistence.statusAsync(store), + status -> sendStatus(ctx, world, status)); + } + private static void sendStatus(@Nonnull CommandContext ctx, + @Nonnull World world, + @Nonnull Status status) { ctx.sendMessage(Message.raw("Impulse persistence status for " + world.getName() + ": runtime spaces=" + status.runtimeSpaces() - + ", persistentBodies=" + + ", runtimeBodies=" + status.runtimePersistentBodies() - + ", runtimeOnlyBodies=" - + status.runtimeOnlyBodies() + ", joints=" + status.runtimeJoints() - + "; stored schema=" + status.schemaVersion() + + "; stored PhysicsStore schema=" + status.schemaVersion() + ", spaces=" + status.storedSpaces() + ", bodies=" + status.storedBodies() + ", joints=" + status.storedJoints() diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/PhysicsChunkExampleCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/PhysicsChunkExampleCommand.java new file mode 100644 index 00000000..e9d38ac4 --- /dev/null +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/PhysicsChunkExampleCommand.java @@ -0,0 +1,206 @@ +package dev.hytalemodding.impulse.examples.commands; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; +import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncPlayerCommand; +import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollision; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionBuildStats; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionPrewarmStats; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionStats; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; + +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; +import org.joml.Vector3d; + +/** + * Debug commands for manually building/clearing PhysicsChunk collision. + */ +public class PhysicsChunkExampleCommand extends AbstractCommandCollection { + + public PhysicsChunkExampleCommand() { + super("physicschunk", "Build PhysicsChunk collision from nearby world blocks"); + addSubCommand(new BuildCommand()); + addSubCommand(new EnsureCommand()); + addSubCommand(new ClearCommand()); + addSubCommand(new StatsCommand()); + } + + @Nonnull + private static Store physicsStore(@Nonnull World world) { + return PhysicsThreading.store(world); + } + + private static final class BuildCommand extends AbstractAsyncPlayerCommand { + + private static final int DEFAULT_RADIUS = 8; + private static final int MAX_RADIUS = 24; + + private final OptionalArg radiusArg = withOptionalArg( + "radius", + "Block radius around the player to scan", + ArgTypes.INTEGER); + private final OptionalArg spaceArg = withOptionalArg( + "space", + "Physics space id to target", + ArgTypes.INTEGER); + + private BuildCommand() { + super("build", "Rebuild nearby PhysicsChunk collision"); + } + + @Nonnull + @Override + protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, + @Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PlayerRef playerRef, + @Nonnull World world) { + Vector3d playerPos = new Vector3d(playerRef.getTransform().getPosition()); + + int radius = ExamplePhysicsUtils.optionalInt(ctx, radiusArg, DEFAULT_RADIUS, 1, MAX_RADIUS); + ExamplePhysicsUtils.SpaceSelection space = + ExamplePhysicsUtils.spaceSelection(ctx, world, spaceArg); + if (space == null) { + return CompletableFuture.completedFuture(null); + } + Store physicsStore = physicsStore(world); + PhysicsChunkCollisionBuildStats stats = PhysicsChunkCollision.rebuildAround(world, + physicsStore, + space.spaceRef(), + playerPos, + radius); + + ctx.sender().sendMessage(Message.raw("Built PhysicsChunk collision: scanned " + + stats.scannedBlocks() + + " blocks, solid " + stats.solidBlocks() + + ", culled " + stats.culledInteriorBlocks() + + ", full runs " + stats.fullCubeRuns() + + ", detail boxes " + stats.detailBoxes() + + ", sections " + stats.sectionsBuilt() + + ", rebuilt " + stats.sectionsRebuilt() + + ", voxel bodies " + stats.voxelBodies() + + ", bodies " + stats.colliderBodies() + + ", removed " + stats.removedBodies() + + ".")); + return CompletableFuture.completedFuture(null); + } + } + + private static final class EnsureCommand extends AbstractAsyncPlayerCommand { + + private static final int DEFAULT_RADIUS = 8; + private static final int MAX_RADIUS = 24; + + private final OptionalArg radiusArg = withOptionalArg( + "radius", + "Block radius around the player to scan", + ArgTypes.INTEGER); + private final OptionalArg spaceArg = withOptionalArg( + "space", + "Physics space id to target", + ArgTypes.INTEGER); + + private EnsureCommand() { + super("ensure", "Ensure nearby PhysicsChunk collision is available"); + } + + @Nonnull + @Override + protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, + @Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PlayerRef playerRef, + @Nonnull World world) { + Vector3d playerPos = new Vector3d(playerRef.getTransform().getPosition()); + int radius = ExamplePhysicsUtils.optionalInt(ctx, radiusArg, DEFAULT_RADIUS, 1, MAX_RADIUS); + ExamplePhysicsUtils.SpaceSelection space = + ExamplePhysicsUtils.spaceSelection(ctx, world, spaceArg); + if (space == null) { + return CompletableFuture.completedFuture(null); + } + + Store physicsStore = physicsStore(world); + PhysicsChunkCollisionPrewarmStats stats = PhysicsChunkCollision.ensureAround(world, + physicsStore, + space.spaceRef(), + List.of(playerPos), + radius, + Math.max(0L, world.getTick())); + + ctx.sender().sendMessage(Message.raw("Ensured PhysicsChunk collision: targets " + + stats.sectionTargets() + + ", bodies " + + stats.buildStats().colliderBodies() + + ", removed " + + stats.buildStats().removedBodies() + + ".")); + return CompletableFuture.completedFuture(null); + } + } + + private static final class ClearCommand extends AbstractAsyncPlayerCommand { + + private final OptionalArg spaceArg = withOptionalArg( + "space", + "Physics space id to target", + ArgTypes.INTEGER); + + private ClearCommand() { + super("clear", "Remove generated PhysicsChunk collision"); + } + + @Nonnull + @Override + protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, + @Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PlayerRef playerRef, + @Nonnull World world) { + ExamplePhysicsUtils.SpaceSelection space = + ExamplePhysicsUtils.spaceSelection(ctx, world, spaceArg); + if (space == null) { + return CompletableFuture.completedFuture(null); + } + Store physicsStore = physicsStore(world); + int removed = PhysicsChunkCollision.clearSpace(world, physicsStore, space.spaceRef()); + ctx.sender().sendMessage(Message.raw("Removed " + removed + + " PhysicsChunk collision bodies.")); + return CompletableFuture.completedFuture(null); + } + } + + private static final class StatsCommand extends AbstractAsyncPlayerCommand { + + private StatsCommand() { + super("stats", "Show generated PhysicsChunk collision stats"); + } + + @Nonnull + @Override + protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, + @Nonnull Store store, + @Nonnull Ref ref, + @Nonnull PlayerRef playerRef, + @Nonnull World world) { + PhysicsChunkCollisionStats stats = PhysicsChunkCollision.stats(world); + ctx.sender().sendMessage(Message.raw("PhysicsChunk collision: " + + stats.spaces() + " spaces, " + + stats.sections() + " sections, " + + stats.bodies() + " bodies, " + + stats.shapeTemplates() + " shape templates.")); + return CompletableFuture.completedFuture(null); + } + } +} diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/EcsCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/PhysicsStoreExampleCommands.java similarity index 50% rename from impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/EcsCommand.java rename to impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/PhysicsStoreExampleCommands.java index 1029d2b4..100291db 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/EcsCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/PhysicsStoreExampleCommands.java @@ -4,139 +4,85 @@ import com.hypixel.hytale.component.Holder; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Transform; import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; import com.hypixel.hytale.server.core.command.system.CommandContext; import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; import com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncPlayerCommand; -import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection; -import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; import com.hypixel.hytale.server.core.modules.time.TimeResource; import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import com.hypixel.hytale.server.core.util.TargetUtil; import dev.hytalemodding.impulse.api.PhysicsBodyType; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyKinematicTargetComponent; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionPrewarmStats; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsEventCollectionMode; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RaycastClosestQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.view.RaycastHitView; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollision; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionPrewarmStats; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsAsync; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsRaycasts; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsWorlds; +import dev.hytalemodding.impulse.core.plugin.components.BodyCommandComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import dev.hytalemodding.impulse.core.plugin.components.UuidComponent; +import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventCollectionMode; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import dev.hytalemodding.impulse.core.plugin.physics.RaycastHitView; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; import dev.hytalemodding.impulse.examples.explosive.ExplosiveBlockComponent; import dev.hytalemodding.impulse.examples.explosive.ExplosiveBlockPolicy; import dev.hytalemodding.impulse.examples.explosive.ExplosiveFuseComponent; import java.util.List; -import java.util.Optional; +import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import dev.hytalemodding.impulse.examples.utils.ExampleBlockEntityVisuals; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; +import org.joml.Quaternionf; import org.joml.Vector3d; import org.joml.Vector3f; import org.joml.Vector3i; -/** - * ECS-first examples for durable rigid body definitions. - */ -public class EcsCommand extends AbstractCommandCollection { +final class PhysicsStoreExampleCommands { private static final double RAY_LENGTH = 24.0; - public EcsCommand() { - super("ecs", "ECS rigid-body examples"); - addSubCommand(new DropCommand()); - addSubCommand(new BumperCommand()); - addSubCommand(new PlatformCommand()); - addSubCommand(new PickupCommand()); - addSubCommand(new WorldCollisionCommand()); - addSubCommand(new ExplosiveCommand()); + private PhysicsStoreExampleCommands() { } - private abstract static class EcsPlayerCommand extends AbstractAsyncPlayerCommand { + abstract static class PhysicsStorePlayerCommand extends AbstractAsyncPlayerCommand { protected final OptionalArg spaceArg = withOptionalArg( "space", "Physics space id to target", ArgTypes.INTEGER); - protected EcsPlayerCommand(@Nonnull String name, @Nonnull String description) { + protected PhysicsStorePlayerCommand(@Nonnull String name, @Nonnull String description) { super(name, description); } @Nullable protected SpaceId resolveSpace(@Nonnull CommandContext ctx, - @Nonnull Store store) { - return ExamplePhysicsUtils.spaceId(ctx, - ExamplePhysicsUtils.resource(store), - spaceArg); - } - } - - private static final class DropCommand extends EcsPlayerCommand { - - private final OptionalArg blockTypeArg = withOptionalArg( - "blockType", - "Hytale block type used for the attached visual entity", - ArgTypes.STRING); - - private DropCommand() { - super("drop", "Spawn a dynamic physics body from split ECS components"); - } - - @Nonnull - @Override - protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, - @Nonnull Store store, - @Nonnull Ref ref, - @Nonnull PlayerRef playerRef, @Nonnull World world) { - Vector3d playerPos = ExamplePhysicsUtils.playerPosition(ctx, store, ref); - if (playerPos == null) { - return CompletableFuture.completedFuture(null); - } - SpaceId spaceId = resolveSpace(ctx, store); - if (spaceId == null) { - return CompletableFuture.completedFuture(null); - } - - RigidBodyKey bodyKey = RigidBodyKey.random(); - Vector3d spawn = new Vector3d(playerPos).add(0.0, 5.0, 0.0); - TimeResource time = store.getResource(TimeResource.getResourceType()); - ExamplePhysicsUtils.spawnPhysicsBodyBlockEntity(store, - time, - bodyKey, - spaceId, - spawn, - blockType(ctx), - PhysicsBodyType.DYNAMIC, - 1.0f, - null); - - ctx.sender().sendMessage(Message.raw("Queued ECS-authored physics body " + bodyKey - + " in space " + spaceId.value() + ".")); - return CompletableFuture.completedFuture(null); - } - - @Nonnull - private String blockType(@Nonnull CommandContext ctx) { - return blockTypeArg.provided(ctx) - ? ExamplePhysicsUtils.resolveBlockType(blockTypeArg.get(ctx)) - : ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE; + return ExamplePhysicsUtils.spaceId(ctx, world, spaceArg); } } - private static final class BumperCommand extends EcsPlayerCommand { + static final class BumperCommand extends PhysicsStorePlayerCommand { private final OptionalArg strengthArg = withOptionalArg( "strength", "Impulse strength", ArgTypes.INTEGER); - private BumperCommand() { + BumperCommand() { super("bumper", "Apply an impulse to the rigid body in the player view"); } @@ -147,41 +93,57 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - SpaceId spaceId = resolveSpace(ctx, store); + SpaceId spaceId = resolveSpace(ctx, world); if (spaceId == null) { return CompletableFuture.completedFuture(null); } - RaycastHitView hit = raycast(ctx, store, ref, spaceId); - if (hit == null || hit.bodyKey() == null) { - ctx.sender().sendMessage(Message.raw("No rigid body in view.")); + Ref spaceRef = ExamplePhysicsUtils.resolveSpaceRef(world, + spaceId); + if (spaceRef == null) { + ctx.sender().sendMessage(Message.raw("PhysicsStore space id=" + spaceId.value() + + " is not bound yet.")); return CompletableFuture.completedFuture(null); } + return PhysicsAsync.acceptOnWorldThread(world, + raycastAsync(store, ref, spaceRef), + hit -> applyImpulse(ctx, store, ref, hit)); + } - TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType()); - if (transform == null) { - ctx.sender().sendMessage(Message.raw("Cannot determine player direction.")); - return CompletableFuture.completedFuture(null); + private void applyImpulse(@Nonnull CommandContext ctx, + @Nonnull Store store, + @Nonnull Ref ref, + @Nullable RaycastHitView hit) { + if (hit == null || hit.bodyRef() == null || !hit.bodyRef().isValid()) { + ctx.sender().sendMessage(Message.raw("No rigid body in view.")); + return; } + int strength = ExamplePhysicsUtils.optionalInt(ctx, strengthArg, 8, 1, 64); - Vector3d impulse = ExamplePhysicsUtils.lookDirection(store, ref, transform).mul(strength); - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - ExamplePhysicsUtils.requireApplied(resource.submitCommands(0L, - 1, - commands -> commands.applyBodyImpulse(hit.bodyKey(), + Vector3d impulse = new Vector3d(TargetUtil.getLook(ref, store) + .getDirection()) + .mul(strength); + Ref bodyRef = hit.bodyRef(); + Store physicsStore = bodyRef.getStore(); + PhysicsBodies.appendCommand(physicsStore, + bodyRef, + BodyCommandComponent.vector(BodyCommandComponent.Kind.IMPULSE, (float) impulse.x, (float) impulse.y, - (float) impulse.z)), "ecs recorder impulse"); - - ctx.sender().sendMessage(Message.raw("Applied ECS recorder impulse to " - + hit.bodyKey() + ".")); - return CompletableFuture.completedFuture(null); + (float) impulse.z, + false, + 0.0f, + 0.0f, + 0.0f)); + + ctx.sender().sendMessage(Message.raw("Queued PhysicsStore impulse command for " + + bodyRef + ".")); } } - private static final class PlatformCommand extends EcsPlayerCommand { + static final class PlatformCommand extends PhysicsStorePlayerCommand { - private PlatformCommand() { - super("platform", "Spawn a kinematic rigid body target from ECS data"); + PlatformCommand() { + super("platform", "Spawn a kinematic PhysicsStore body target"); } @Nonnull @@ -191,38 +153,61 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - Vector3d playerPos = ExamplePhysicsUtils.playerPosition(ctx, store, ref); - if (playerPos == null) { - return CompletableFuture.completedFuture(null); - } - SpaceId spaceId = resolveSpace(ctx, store); + Vector3d playerPos = new Vector3d(playerRef.getTransform().getPosition()); + SpaceId spaceId = resolveSpace(ctx, world); if (spaceId == null) { return CompletableFuture.completedFuture(null); } - RigidBodyKey bodyKey = RigidBodyKey.random(); + UUID bodyUuid = UUID.randomUUID(); Vector3d spawn = new Vector3d(playerPos).add(0.0, 2.0, 0.0); - PhysicsBodyKinematicTargetComponent target = ExamplePhysicsUtils.kinematicTargetAt(spawn); - TimeResource time = store.getResource(TimeResource.getResourceType()); - ExamplePhysicsUtils.spawnPhysicsBodyBlockEntity(store, - time, - bodyKey, - spaceId, - spawn, - ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE, - PhysicsBodyType.KINEMATIC, + Ref spaceRef = ExamplePhysicsUtils.resolveSpaceRef(world, + spaceId); + if (spaceRef == null) { + ctx.sender().sendMessage(Message.raw("PhysicsStore space id=" + spaceId.value() + + " is not bound yet.")); + return CompletableFuture.completedFuture(null); + } + Vector3f targetPosition = vector(spawn); + Holder bodyHolder = ExamplePhysicsUtils.bodyEntity(spaceRef, + bodyUuid, + targetPosition, + PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f), 0.0f, - target); + RigidBodySpawnSettings.material(0.5f, 0.2f), + null); + bodyHolder.tryRemoveComponent(DynamicsComponent.getComponentType()); + bodyHolder.addComponent(DynamicsComponent.getComponentType(), + new DynamicsComponent(PhysicsBodyType.KINEMATIC, + 0.0f, + 0.0f, + 0.0f, + false)); + bodyHolder.tryRemoveComponent(TargetComponent.getComponentType()); + bodyHolder.addComponent(TargetComponent.getComponentType(), target(targetPosition)); + var bodyRef = ExamplePhysicsUtils.addPhysicsStoreBody(world, bodyHolder); - ctx.sender().sendMessage(Message.raw("Queued ECS kinematic platform " + bodyKey + TimeResource time = store.getResource(TimeResource.getResourceType()); + ExamplePhysicsUtils.attachBlockBody(store, + time, + new ExamplePhysicsUtils.CreatedBlockBody(bodyUuid, + bodyRef, + spaceId, + ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE, + (float) spawn.x, + (float) spawn.y, + (float) spawn.z, + false)); + + ctx.sender().sendMessage(Message.raw("Queued PhysicsStore kinematic platform " + bodyUuid + " in space " + spaceId.value() + ".")); return CompletableFuture.completedFuture(null); } } - private static final class PickupCommand extends EcsPlayerCommand { + static final class PickupCommand extends PhysicsStorePlayerCommand { - private PickupCommand() { + PickupCommand() { super("pickup", "Attach a view entity to the physics body in view"); } @@ -233,78 +218,57 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - SpaceId spaceId = resolveSpace(ctx, store); + SpaceId spaceId = resolveSpace(ctx, world); if (spaceId == null) { return CompletableFuture.completedFuture(null); } - RaycastHitView hit = raycast(ctx, store, ref, spaceId); - if (hit == null || hit.bodyKey() == null) { - ctx.sender().sendMessage(Message.raw("No rigid body in view.")); + Ref spaceRef = ExamplePhysicsUtils.resolveSpaceRef(world, + spaceId); + if (spaceRef == null) { + ctx.sender().sendMessage(Message.raw("PhysicsStore space id=" + spaceId.value() + + " is not bound yet.")); return CompletableFuture.completedFuture(null); } + return PhysicsAsync.acceptOnWorldThread(world, + raycastAsync(store, ref, spaceRef), + hit -> attachView(ctx, store, hit)); + } + + private static void attachView(@Nonnull CommandContext ctx, + @Nonnull Store store, + @Nullable RaycastHitView hit) { + if (hit == null || hit.bodyRef() == null || !hit.bodyRef().isValid()) { + ctx.sender().sendMessage(Message.raw("No rigid body in view.")); + return; + } + UUID bodyUuid = physicsStoreBodyUuid(hit.bodyRef()); + if (bodyUuid == null) { + ctx.sender().sendMessage(Message.raw("PhysicsStore body has no persistent UUID.")); + return; + } Vector3d point = new Vector3d(hit.point().x, hit.point().y, hit.point().z); TimeResource time = store.getResource(TimeResource.getResourceType()); ExamplePhysicsUtils.spawnExternalBodyViewBlockEntity(store, time, - hit.bodyKey(), - spaceId, + hit.bodyRef(), + bodyUuid, point, ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE); - ctx.sender().sendMessage(Message.raw("Attached view-only ECS entity to " - + hit.bodyKey() + ".")); - return CompletableFuture.completedFuture(null); - } - } - - private static final class WorldCollisionCommand extends EcsPlayerCommand { - - private final OptionalArg radiusArg = withOptionalArg( - "radius", - "Block radius around the player to scan", - ArgTypes.INTEGER); - - private WorldCollisionCommand() { - super("world-collision", "Ensure voxel collision around the player for an explicit space"); + ctx.sender().sendMessage(Message.raw("Attached view-only entity to " + + bodyUuid + ".")); } - @Nonnull - @Override - protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, - @Nonnull Store store, - @Nonnull Ref ref, - @Nonnull PlayerRef playerRef, - @Nonnull World world) { - Vector3d playerPos = ExamplePhysicsUtils.playerPosition(ctx, store, ref); - if (playerPos == null) { - return CompletableFuture.completedFuture(null); - } - SpaceId spaceId = resolveSpace(ctx, store); - if (spaceId == null) { - return CompletableFuture.completedFuture(null); - } - - int radius = ExamplePhysicsUtils.optionalInt(ctx, radiusArg, 8, 1, 24); - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - WorldCollisionPrewarmStats stats = resource.ensureWorldCollisionAround(world, - spaceId, - List.of(playerPos), - radius, - 0L); - - ctx.sender().sendMessage(Message.raw("Ensured ECS world collision: targets " - + stats.sectionTargets() - + ", bodies " - + stats.buildStats().colliderBodies() - + ", removed " - + stats.buildStats().removedBodies() - + ".")); - return CompletableFuture.completedFuture(null); + @Nullable + private static UUID physicsStoreBodyUuid(@Nonnull Ref bodyRef) { + UuidComponent uuid = bodyRef.getStore().getComponent(bodyRef, + UuidComponent.getComponentType()); + return uuid != null ? uuid.getUuid() : null; } } - private static final class ExplosiveCommand extends EcsPlayerCommand { + static final class ExplosiveCommand extends PhysicsStorePlayerCommand { private static final double FALLBACK_SPAWN_DISTANCE = 5.0; private static final int DEFAULT_EXPLOSION_RADIUS = 8; @@ -320,7 +284,7 @@ private static final class ExplosiveCommand extends EcsPlayerCommand { ArgTypes.STRING); private final OptionalArg radiusArg = withOptionalArg( "radius", - "Block radius fragmented when the explosive block hits world collision", + "Block radius fragmented when the explosive block hits PhysicsChunk collision", ArgTypes.INTEGER); private final OptionalArg maxFragmentsArg = withOptionalArg( "maxFragments", @@ -335,8 +299,8 @@ private static final class ExplosiveCommand extends EcsPlayerCommand { "Upward lift fraction applied to spawned fragments", ArgTypes.FLOAT); - private ExplosiveCommand() { - super("explosive", "Drop an ECS explosive block that fragments terrain on impact"); + ExplosiveCommand() { + super("explosive", "Drop an explosive block that fragments terrain on impact"); } @Nonnull @@ -346,18 +310,13 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType()); - if (transform == null) { - ctx.sender().sendMessage(Message.raw("Cannot determine player position.")); - return CompletableFuture.completedFuture(null); - } - SpaceId spaceId = resolveSpace(ctx, store); + SpaceId spaceId = resolveSpace(ctx, world); if (spaceId == null) { return CompletableFuture.completedFuture(null); } String blockType = blockTypeArg.provided(ctx) - ? ExamplePhysicsUtils.resolveBlockType(blockTypeArg.get(ctx)) + ? ExampleBlockEntityVisuals.resolveBlockType(blockTypeArg.get(ctx)) : ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE; int radius = ExamplePhysicsUtils.optionalInt(ctx, radiusArg, @@ -372,20 +331,37 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, float strength = optionalFloat(ctx, strengthArg, 12.0f, 0.0f, 128.0f); float verticalLift = optionalFloat(ctx, verticalLiftArg, 0.35f, 0.0f, 2.0f); - Vector3d spawn = spawnPosition(store, ref, transform, world); + Vector3d spawn = spawnPosition(store, ref, world); if (blockType(blockType) == null) { ctx.sender().sendMessage(Message.raw("No valid explosive block type is available.")); return CompletableFuture.completedFuture(null); } - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - ensureContactEvents(resource); - WorldCollisionPrewarmStats stats = resource.ensureWorldCollisionAround(world, - spaceId, + Store physicsStore = + PhysicsThreading.store(world); + boolean contactEventsEnabled = contactEventsEnabled(physicsStore); + Ref spaceRef = ExamplePhysicsUtils.resolveSpaceRef(world, + spaceId); + if (spaceRef == null) { + ctx.sender().sendMessage(Message.raw("PhysicsStore space id=" + spaceId.value() + + " is not bound yet.")); + return CompletableFuture.completedFuture(null); + } + PhysicsChunkCollisionPrewarmStats stats = PhysicsChunkCollision.ensureAround(world, + physicsStore, + spaceRef, List.of(spawn), Math.max(8, radius + 6), Math.max(0L, world.getTick())); - RigidBodyKey bodyKey = RigidBodyKey.random(); + UUID bodyUuid = UUID.randomUUID(); + Ref bodyRef = ExamplePhysicsUtils.addPhysicsStoreBody(world, + ExamplePhysicsUtils.bodyEntity(spaceRef, + bodyUuid, + vector(spawn), + PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f), + 1.0f, + RigidBodySpawnSettings.material(SOURCE_FRICTION, SOURCE_RESTITUTION), + null)); TimeResource time = store.getResource(TimeResource.getResourceType()); ExplosiveBlockComponent settings = new ExplosiveBlockComponent(blockType, 0, @@ -394,25 +370,25 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, maxFragments, strength, verticalLift); - Holder holder = ExamplePhysicsUtils.physicsBodyBlockEntityHolder(time, - bodyKey, - spaceId, - spawn, + Holder holder = ExamplePhysicsUtils.attachedPhysicsBlockEntityHolder(time, + bodyRef, + bodyUuid, blockType, - PhysicsBodyType.DYNAMIC, - 1.0f, - SOURCE_FRICTION, - SOURCE_RESTITUTION, - null); + spawn, + new Vector3f(), + new Quaternionf(), + Float.NaN, + true); holder.addComponent(ExplosiveBlockComponent.getComponentType(), settings); holder.addComponent(ExplosiveFuseComponent.getComponentType(), new ExplosiveFuseComponent()); store.addEntity(holder, AddReason.SPAWN); - ctx.sender().sendMessage(Message.raw("Queued Impulse explosive body " + bodyKey + ctx.sender().sendMessage(Message.raw("Queued Impulse explosive body " + bodyUuid + " in space " + spaceId.value() + " radius=" + radius + " maxFragments=" + maxFragments + " collisionBodies=" + stats.buildStats().colliderBodies() + + " contactEvents=" + contactEventsEnabled + ".")); return CompletableFuture.completedFuture(null); } @@ -426,22 +402,18 @@ private static BlockType blockType(@Nonnull String blockTypeId) { return BlockType.getAssetMap().getAsset(ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE); } - private static void ensureContactEvents(@Nonnull PhysicsWorldResource resource) { - PhysicsWorldSettings settings = resource.getWorldSettings(); - if (settings.getEventCollectionMode() == PhysicsEventCollectionMode.CONTACTS) { - return; - } - settings.setEventCollectionMode(PhysicsEventCollectionMode.CONTACTS); - resource.setWorldSettings(settings); + private static boolean contactEventsEnabled(@Nonnull Store physicsStore) { + return PhysicsWorlds.settings(physicsStore).getEventCollectionMode() + == PhysicsEventCollectionMode.CONTACTS; } @Nonnull private static Vector3d spawnPosition(@Nonnull Store store, @Nonnull Ref ref, - @Nonnull TransformComponent transform, @Nonnull World world) { - Vector3d eye = ExamplePhysicsUtils.eyePosition(store, ref, transform); - Vector3d direction = ExamplePhysicsUtils.lookDirection(store, ref, transform); + Transform look = TargetUtil.getLook(ref, store); + Vector3d eye = new Vector3d(look.getPosition()); + Vector3d direction = new Vector3d(look.getDirection()); Vector3i target = TargetUtil.getTargetBlock(world, ExplosiveBlockPolicy::isSimpleFullCubeFragmentBlock, eye.x, @@ -475,26 +447,33 @@ private static float optionalFloat(@Nonnull CommandContext ctx, } } - @Nullable - private static RaycastHitView raycast(@Nonnull CommandContext ctx, - @Nonnull Store store, + @Nonnull + private static CompletionStage raycastAsync(@Nonnull Store store, @Nonnull Ref ref, - @Nonnull SpaceId spaceId) { - TransformComponent transform = store.getComponent(ref, TransformComponent.getComponentType()); - if (transform == null) { - ctx.sender().sendMessage(Message.raw("Cannot determine player position.")); - return null; - } - - Vector3d start = ExamplePhysicsUtils.eyePosition(store, ref, transform); + @Nonnull Ref spaceRef) { + Transform look = TargetUtil.getLook(ref, store); + Vector3d start = new Vector3d(look.getPosition()); Vector3d end = new Vector3d(start) - .add(ExamplePhysicsUtils.lookDirection(store, ref, transform).mul(RAY_LENGTH)); - Optional hit = ExamplePhysicsUtils.resource(store) - .query(new RaycastClosestQuery(spaceId, vector(start), vector(end))) - .completion() - .toCompletableFuture() - .join(); - return hit.orElse(null); + .add(new Vector3d(look.getDirection()).mul(RAY_LENGTH)); + return PhysicsRaycasts.closestAsync(store.getExternalData().getWorld(), + spaceRef, + vector(start), + vector(end)) + .thenApply(hit -> hit.orElse(null)); + } + + @Nonnull + private static TargetComponent target(@Nonnull Vector3f targetPosition) { + TargetComponent target = new TargetComponent(); + target.setActive(true); + target.setPosition(targetPosition); + target.setRotation(new Quaternionf()); + target.setLinearVelocity(new Vector3f()); + target.setAngularVelocity(new Vector3f()); + target.setTransformEnabled(true); + target.setVelocityEnabled(false); + target.setActivate(true); + return target; } @Nonnull diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/RaycastCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/RaycastCommand.java index ca2af605..be521649 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/RaycastCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/RaycastCommand.java @@ -1,31 +1,29 @@ package dev.hytalemodding.impulse.examples.commands; -import com.hypixel.hytale.component.ComponentType; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.vector.Transform; import com.hypixel.hytale.server.core.Message; import com.hypixel.hytale.server.core.command.system.CommandContext; import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; import com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncPlayerCommand; import com.hypixel.hytale.server.core.modules.debug.DebugUtils; -import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RaycastClosestQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.view.RaycastHitView; +import com.hypixel.hytale.server.core.util.TargetUtil; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsAsync; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsRaycasts; +import dev.hytalemodding.impulse.core.plugin.physics.RaycastHitView; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; import org.joml.Vector3d; public class RaycastCommand extends AbstractAsyncPlayerCommand { private static final double RAY_LENGTH = 24.0; - private static final ComponentType TRANSFORM_TYPE = - TransformComponent.getComponentType(); private final OptionalArg spaceArg = this.withOptionalArg( "space", "Physics space id to target", @@ -42,35 +40,34 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - TransformComponent transform = store.getComponent(ref, TRANSFORM_TYPE); - if (transform == null) { - ctx.sender().sendMessage(Message.raw("Cannot determine player position.")); + ExamplePhysicsUtils.SpaceSelection space = ExamplePhysicsUtils.spaceSelection(ctx, + world, + spaceArg); + if (space == null) { return CompletableFuture.completedFuture(null); } - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, resource, spaceArg); - if (spaceId == null) { - return CompletableFuture.completedFuture(null); - } - - Vector3d start = ExamplePhysicsUtils.eyePosition(store, ref, transform); - Vector3d direction = ExamplePhysicsUtils.lookDirection(store, ref, transform).mul(RAY_LENGTH); + Transform look = TargetUtil.getLook(ref, store); + Vector3d start = new Vector3d(look.getPosition()); + Vector3d direction = new Vector3d(look.getDirection()).mul(RAY_LENGTH); Vector3d end = new Vector3d(start).add(direction); DebugUtils.addArrow(world, start, direction, DebugUtils.COLOR_WHITE, 0.8f, 4.0f, DebugUtils.FLAG_FADE); - RaycastResult hit = resource.query(new RaycastClosestQuery(spaceId, + return PhysicsAsync.acceptOnWorldThread(world, + PhysicsRaycasts.closestAsync(world, + space.spaceRef(), ExamplePhysicsUtils.toVector3f(start), - ExamplePhysicsUtils.toVector3f(end))) - .completion() - .toCompletableFuture() - .join() - .map(RaycastCommand::toResult) - .orElse(null); + ExamplePhysicsUtils.toVector3f(end)), + hit -> handleHit(ctx, world, hit.map(RaycastCommand::toResult).orElse(null))); + } + + private static void handleHit(@Nonnull CommandContext ctx, + @Nonnull World world, + RaycastResult hit) { if (hit == null) { ctx.sender().sendMessage(Message.raw("Physics ray missed.")); - return CompletableFuture.completedFuture(null); + return; } Vector3d hitPoint = hit.point(); @@ -83,7 +80,6 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, ctx.sender().sendMessage(Message.raw("Physics ray hit " + hit.shapeType() + " at distance " + hit.distance())); - return CompletableFuture.completedFuture(null); } private record RaycastResult(@Nonnull Vector3d point, diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ReleaseCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ReleaseCommand.java index 7a985287..d6f17d94 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ReleaseCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ReleaseCommand.java @@ -8,7 +8,7 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.plugin.modules.control.PhysicsControlSessions; +import dev.hytalemodding.impulse.builtin.control.PhysicsControlSessions; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ShapesCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ShapesCommand.java index fba299b0..33f73ea5 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ShapesCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ShapesCommand.java @@ -1,5 +1,6 @@ package dev.hytalemodding.impulse.examples.commands; +import com.hypixel.hytale.component.AddReason; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.Message; @@ -11,14 +12,19 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodyEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; +import org.joml.Quaternionf; import org.joml.Vector3d; +import org.joml.Vector3f; public class ShapesCommand extends AbstractAsyncPlayerCommand { @@ -38,46 +44,90 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - Vector3d playerPos = ExamplePhysicsUtils.playerPosition(ctx, store, ref); - if (playerPos == null) { - return CompletableFuture.completedFuture(null); - } + Vector3d playerPos = new Vector3d(playerRef.getTransform().getPosition()); - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, resource, spaceArg); - if (spaceId == null) { + ExamplePhysicsUtils.SpaceSelection space = ExamplePhysicsUtils.spaceSelection(ctx, + world, + spaceArg); + if (space == null) { return CompletableFuture.completedFuture(null); } TimeResource time = store.getResource(TimeResource.getResourceType()); + Store physicsStore = PhysicsThreading.store(world); + PhysicsThreading.requireWorldThread(physicsStore, + "spawn shape example PhysicsStore body entities"); Vector3d origin = new Vector3d(playerPos).add(-4.0, 3.0, 3.0); - spawn(store, time, resource, spaceId, ShapeType.BOX, PhysicsAxis.Y, + spawn(store, + physicsStore, + time, + space.spaceRef(), + ShapeType.BOX, + PhysicsAxis.Y, origin, 0); - spawn(store, time, resource, spaceId, ShapeType.SPHERE, PhysicsAxis.Y, origin, 2); - spawn(store, time, resource, spaceId, ShapeType.CAPSULE, PhysicsAxis.Y, origin, 4); - spawn(store, time, resource, spaceId, ShapeType.CYLINDER, PhysicsAxis.Y, origin, 6); - spawn(store, time, resource, spaceId, ShapeType.CONE, PhysicsAxis.Y, origin, 8); + spawn(store, + physicsStore, + time, + space.spaceRef(), + ShapeType.SPHERE, + PhysicsAxis.Y, + origin, 2); + spawn(store, + physicsStore, + time, + space.spaceRef(), + ShapeType.CAPSULE, + PhysicsAxis.Y, + origin, 4); + spawn(store, + physicsStore, + time, + space.spaceRef(), + ShapeType.CYLINDER, + PhysicsAxis.Y, + origin, 6); + spawn(store, + physicsStore, + time, + space.spaceRef(), + ShapeType.CONE, + PhysicsAxis.Y, + origin, 8); ctx.sender().sendMessage(Message.raw("Spawned shape demo.")); return CompletableFuture.completedFuture(null); } private static void spawn(@Nonnull Store store, + @Nonnull Store physicsStore, @Nonnull TimeResource time, - @Nonnull PhysicsWorldResource resource, - @Nonnull SpaceId spaceId, + @Nonnull Ref spaceRef, @Nonnull ShapeType type, @Nonnull PhysicsAxis axis, @Nonnull Vector3d origin, int xOffset) { - ExamplePhysicsUtils.spawnBlockBody(store, - time, - resource, - spaceId, - new Vector3d(origin).add(xOffset, 0.0, 0.0), + Vector3d position = new Vector3d(origin).add(xOffset, 0.0, 0.0); + UUID bodyUuid = UUID.randomUUID(); + var bodyHolder = PhysicsBodyEntities.dynamicBodyHolder(spaceRef, + bodyUuid, + ExamplePhysicsUtils.toVector3f(position), shape(type, axis), 1.0f, - RigidBodySpawnSettings.material(0.7f, 0.35f)); + RigidBodySpawnSettings.material(0.7f, 0.35f), + null); + Ref bodyRef = physicsStore.addEntity(bodyHolder, AddReason.SPAWN); + assert bodyRef != null; + store.addEntity(ExamplePhysicsUtils.attachedPhysicsBlockEntityHolder( + time, + bodyRef, + bodyUuid, + ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE, + position, + new Vector3f(), + new Quaternionf(), + Float.NaN, + true), + AddReason.SPAWN); } @Nonnull diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/WorldCollisionCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/WorldCollisionCommand.java deleted file mode 100644 index 42c69223..00000000 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/WorldCollisionCommand.java +++ /dev/null @@ -1,144 +0,0 @@ -package dev.hytalemodding.impulse.examples.commands; - -import com.hypixel.hytale.component.Ref; -import com.hypixel.hytale.component.Store; -import com.hypixel.hytale.server.core.Message; -import com.hypixel.hytale.server.core.command.system.CommandContext; -import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; -import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes; -import com.hypixel.hytale.server.core.command.system.basecommands.AbstractAsyncPlayerCommand; -import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection; -import com.hypixel.hytale.server.core.universe.PlayerRef; -import com.hypixel.hytale.server.core.universe.world.World; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionBuildStats; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionStats; -import java.util.concurrent.CompletableFuture; -import javax.annotation.Nonnull; -import org.joml.Vector3d; - -/** - * Debug commands for manually building/clearing world voxel collision. - */ -public class WorldCollisionCommand extends AbstractCommandCollection { - - public WorldCollisionCommand() { - super("world-collision", "Build static Impulse voxel collision from nearby world blocks"); - addSubCommand(new BuildCommand()); - addSubCommand(new ClearCommand()); - addSubCommand(new StatsCommand()); - } - - private static final class BuildCommand extends AbstractAsyncPlayerCommand { - - private static final int DEFAULT_RADIUS = 8; - private static final int MAX_RADIUS = 24; - - private final OptionalArg radiusArg = withOptionalArg( - "radius", - "Block radius around the player to scan", - ArgTypes.INTEGER); - private final OptionalArg spaceArg = withOptionalArg( - "space", - "Physics space id to target", - ArgTypes.INTEGER); - - private BuildCommand() { - super("build", "Rebuild nearby static voxel collision"); - } - - @Nonnull - @Override - protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, - @Nonnull Store store, - @Nonnull Ref ref, - @Nonnull PlayerRef playerRef, - @Nonnull World world) { - Vector3d playerPos = ExamplePhysicsUtils.playerPosition(ctx, store, ref); - if (playerPos == null) { - return CompletableFuture.completedFuture(null); - } - - int radius = ExamplePhysicsUtils.optionalInt(ctx, radiusArg, DEFAULT_RADIUS, 1, MAX_RADIUS); - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, resource, spaceArg); - if (spaceId == null) { - return CompletableFuture.completedFuture(null); - } - WorldCollisionBuildStats stats = resource.rebuildWorldCollisionAround(world, - spaceId, - playerPos, - radius); - - ctx.sender().sendMessage(Message.raw("Built world voxel collision: scanned " - + stats.scannedBlocks() - + " blocks, solid " + stats.solidBlocks() - + ", culled " + stats.culledInteriorBlocks() - + ", full runs " + stats.fullCubeRuns() - + ", detail boxes " + stats.detailBoxes() - + ", sections " + stats.sectionsBuilt() - + ", rebuilt " + stats.sectionsRebuilt() - + ", voxel bodies " + stats.voxelBodies() - + ", bodies " + stats.colliderBodies() - + ", removed " + stats.removedBodies() - + ".")); - return CompletableFuture.completedFuture(null); - } - } - - private static final class ClearCommand extends AbstractAsyncPlayerCommand { - - private final OptionalArg spaceArg = withOptionalArg( - "space", - "Physics space id to target", - ArgTypes.INTEGER); - - private ClearCommand() { - super("clear", "Remove generated static voxel collision"); - } - - @Nonnull - @Override - protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, - @Nonnull Store store, - @Nonnull Ref ref, - @Nonnull PlayerRef playerRef, - @Nonnull World world) { - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, resource, spaceArg); - if (spaceId == null) { - return CompletableFuture.completedFuture(null); - } - int removed = resource.clearWorldCollision(spaceId); - ctx.sender().sendMessage(Message.raw("Removed " + removed - + " world voxel collision bodies.")); - return CompletableFuture.completedFuture(null); - } - } - - private static final class StatsCommand extends AbstractAsyncPlayerCommand { - - private StatsCommand() { - super("stats", "Show generated static voxel collision stats"); - } - - @Nonnull - @Override - protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, - @Nonnull Store store, - @Nonnull Ref ref, - @Nonnull PlayerRef playerRef, - @Nonnull World world) { - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - WorldCollisionStats stats = resource.getWorldCollisionStats(); - ctx.sender().sendMessage(Message.raw("World voxel collision: " - + stats.spaces() + " spaces, " - + stats.sections() + " sections, " - + stats.bodies() + " bodies, " - + stats.shapeTemplates() + " shape templates.")); - return CompletableFuture.completedFuture(null); - } - } -} diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/BlockBodyBatchBuilder.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/BlockBodyBatchBuilder.java new file mode 100644 index 00000000..2ca49b19 --- /dev/null +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/BlockBodyBatchBuilder.java @@ -0,0 +1,113 @@ +package dev.hytalemodding.impulse.examples.commands.stress; + +import java.util.Arrays; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; + +final class BlockBodyBatchBuilder { + + private static final int POSITION_STRIDE = 3; + + private final long bodyUuidRunId = UUID.randomUUID().getMostSignificantBits(); + private long[] bodyUuidMostSignificantBits; + private long[] bodyUuidLeastSignificantBits; + private float[] positions; + private int size; + private boolean sealed; + + BlockBodyBatchBuilder(int expectedBodies) { + int capacity = Math.max(1, expectedBodies); + bodyUuidMostSignificantBits = new long[capacity]; + bodyUuidLeastSignificantBits = new long[capacity]; + positions = new float[capacity * POSITION_STRIDE]; + } + + @Nonnull + BlockBodyBatchBuilder addBody(float positionX, + float positionY, + float positionZ) { + return addBody(bodyUuidRunId, + size + 1L, + positionX, + positionY, + positionZ); + } + + @Nonnull + BlockBodyBatchBuilder addBody(long bodyUuidMostSignificantBits, + long bodyUuidLeastSignificantBits, + float positionX, + float positionY, + float positionZ) { + assertMutable(); + ensureCapacity(size + 1); + this.bodyUuidMostSignificantBits[size] = bodyUuidMostSignificantBits; + this.bodyUuidLeastSignificantBits[size] = bodyUuidLeastSignificantBits; + int positionOffset = size * POSITION_STRIDE; + positions[positionOffset] = positionX; + positions[positionOffset + 1] = positionY; + positions[positionOffset + 2] = positionZ; + size++; + return this; + } + + void seal() { + sealed = true; + } + + boolean isEmpty() { + return size == 0; + } + + int size() { + return size; + } + + @Nonnull + UUID bodyUuid(int index) { + checkIndex(index); + return new UUID(bodyUuidMostSignificantBits[index], + bodyUuidLeastSignificantBits[index]); + } + + float positionX(int index) { + return position(index, 0); + } + + float positionY(int index) { + return position(index, 1); + } + + float positionZ(int index) { + return position(index, 2); + } + + private float position(int index, int slot) { + checkIndex(index); + return positions[index * POSITION_STRIDE + slot]; + } + + private void ensureCapacity(int required) { + if (required <= bodyUuidMostSignificantBits.length) { + return; + } + int nextCapacity = Math.max(required, + bodyUuidMostSignificantBits.length + (bodyUuidMostSignificantBits.length >> 1) + 1); + bodyUuidMostSignificantBits = Arrays.copyOf(bodyUuidMostSignificantBits, nextCapacity); + bodyUuidLeastSignificantBits = Arrays.copyOf(bodyUuidLeastSignificantBits, nextCapacity); + positions = Arrays.copyOf(positions, nextCapacity * POSITION_STRIDE); + } + + private void checkIndex(int index) { + if (index < 0 || index >= size) { + throw new IndexOutOfBoundsException(index); + } + } + + private void assertMutable() { + if (sealed) { + throw new IllegalStateException("Block body batch builder is already sealed"); + } + } +} diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressBenchmarkCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressBenchmarkCommand.java index 3082463e..26b27d89 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressBenchmarkCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressBenchmarkCommand.java @@ -11,16 +11,14 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodyType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.core.plugin.simulation.query.SpaceBodyCountQuery; -import dev.hytalemodding.impulse.examples.commands.ExamplePhysicsUtils; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsAsync; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsDiagnostics; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import dev.hytalemodding.impulse.examples.utils.ExampleBlockEntityVisuals; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; import java.util.Locale; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; @@ -39,7 +37,7 @@ public class StressBenchmarkCommand extends AbstractAsyncPlayerCommand { private final OptionalArg modeArg = this.withOptionalArg( "mode", - "Benchmark mode: raw or entity", + "Benchmark mode: raw PhysicsStore rows or entity visuals", ArgTypes.STRING); private final OptionalArg countArg = this.withOptionalArg( "count", @@ -47,7 +45,7 @@ public class StressBenchmarkCommand extends AbstractAsyncPlayerCommand { ArgTypes.INTEGER); private final OptionalArg blockTypeArg = this.withOptionalArg( "blockType", - "Hytale block type for entity-backed benchmark visuals", + "Hytale block type for entity visual benchmark rows", ArgTypes.STRING); private final OptionalArg spaceArg = this.withOptionalArg( "space", @@ -55,7 +53,7 @@ public class StressBenchmarkCommand extends AbstractAsyncPlayerCommand { ArgTypes.INTEGER); public StressBenchmarkCommand() { - super("benchmark", "Spawn repeatable raw or entity-backed benchmark body grids"); + super("benchmark", "Spawn repeatable PhysicsStore row or entity visual body grids"); } @Nonnull @@ -70,55 +68,70 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, return CompletableFuture.completedFuture(null); } - Vector3d playerPos = ExamplePhysicsUtils.playerPosition(ctx, store, ref); - if (playerPos == null) { - return CompletableFuture.completedFuture(null); - } + Vector3d playerPos = new Vector3d(playerRef.getTransform().getPosition()); - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, resource, spaceArg); + SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, world, spaceArg); if (spaceId == null) { return CompletableFuture.completedFuture(null); } + Ref spaceRef = ExamplePhysicsUtils.resolveSpaceRef(world, spaceId); + if (spaceRef == null) { + ctx.sender().sendMessage(Message.raw("PhysicsStore space id=" + spaceId.value() + + " is not bound.")); + return CompletableFuture.completedFuture(null); + } BenchmarkLayout layout = BenchmarkLayout.around(playerPos, request.count()); - int beforeBodies = resource.query(new SpaceBodyCountQuery(spaceId)) - .completion() - .toCompletableFuture() - .join(); + return PhysicsAsync.acceptOnWorldThread(world, + PhysicsDiagnostics.bodyCountAsync(world, spaceRef), + beforeBodies -> spawnBenchmark(ctx, + store, + world, + spaceRef, + spaceId, + request, + layout, + beforeBodies)); + } + private static void spawnBenchmark(@Nonnull CommandContext ctx, + @Nonnull Store store, + @Nonnull World world, + @Nonnull Ref spaceRef, + @Nonnull SpaceId spaceId, + @Nonnull BenchmarkRequest request, + @Nonnull BenchmarkLayout layout, + int beforeBodies) { long serverTick = Math.max(0L, world.getTick()); BenchmarkSpawnTiming timing = switch (request.mode()) { - case RAW -> spawnRaw(resource, spaceId, layout, request.count(), serverTick); + case RAW -> spawnRaw(world, spaceRef, spaceId, layout, request.count()); case ENTITY -> spawnEntities(store, - resource, + spaceRef, spaceId, layout, request.count(), request.blockType(), serverTick); }; - int afterBodies = resource.query(new SpaceBodyCountQuery(spaceId)) - .completion() - .toCompletableFuture() - .join(); if (timing.spawned() > 0) { - ctx.sender().sendMessage(Message.raw("Spawned " + timing.spawned() + " " + ctx.sender().sendMessage(Message.raw("Added " + timing.spawned() + " " + request.mode().label() + " benchmark bodies: setupWallMs=" + millis(timing.setupWallNanos()) - + " commandApplyMs=" + millis(timing.commandApplyNanos()) - + (timing.entityAttachNanos() > 0L - ? " entityAttachMs=" + millis(timing.entityAttachNanos()) + + " physicsStoreApplyMs=" + millis(timing.physicsStoreApplyNanos()) + + (timing.visualAttachNanos() > 0L + ? " visualAttachMs=" + millis(timing.visualAttachNanos()) : "") + " (" + microsPerBody(timing.setupWallNanos(), timing.spawned()) - + " us/body). Space bodies: " + beforeBodies + " -> " + afterBodies + + " us/body). Space bodies before add: " + beforeBodies + (request.mode() == BenchmarkMode.ENTITY ? ". blockType=" + request.blockType() : "") - + ". This command measures raw setup/entity attachment; use /impulse-examples stress bodies" - + " for detached/detached-view scalability scenarios" - + ". For clean comparisons run /impulse clean, /impulse-world-collision perf reset," - + " /impulse-world-collision perf toggle before spawning, then /impulse-world-collision perf report.")); + + ". Body-count updates are visible after PhysicsStore binds the new rows" + + ". This command measures raw PhysicsStore row setup or entity visual attachment;" + + " use /impulse-examples stress bodies detached-view" + + " for scalable detached visual diagnostics" + + ". For clean comparisons run /impulse clean, /impulse physicschunk perf reset," + + " /impulse physicschunk perf toggle before spawning," + + " then /impulse physicschunk perf report.")); } - return CompletableFuture.completedFuture(null); } @Nullable @@ -145,40 +158,37 @@ private BenchmarkRequest parseRequest(@Nonnull CommandContext ctx) { } @Nonnull - private static BenchmarkSpawnTiming spawnRaw(@Nonnull PhysicsWorldResource resource, + private static BenchmarkSpawnTiming spawnRaw(@Nonnull World world, + @Nonnull Ref spaceRef, @Nonnull SpaceId spaceId, @Nonnull BenchmarkLayout layout, - int count, - long serverTick) { + int count) { PhysicsShapeSpec box = PhysicsShapeSpec.box(0.48f, 0.48f, 0.48f); RigidBodySpawnSettings spawnSettings = RigidBodySpawnSettings.material(0.65f, 0.15f); - long bodyKeyRunId = RigidBodyKey.random().mostSignificantBits(); - long commandStartNanos = System.nanoTime(); - ExamplePhysicsUtils.requireApplied(resource.submitCommands(serverTick, 1, commands -> - commands.spawnBodies(count, + StressBodyBatches.BodyEntityBatchTiming timing = + StressBodyBatches.addDynamicBodyBatchMeasured(world, + spaceRef, spaceId, + count, box, 1.0f, - PhysicsBodyType.DYNAMIC, spawnSettings, - PhysicsBodyKind.TEMPORARY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY, - spawns -> { + bodies -> { for (int i = 0; i < count; i++) { - spawns.body(bodyKeyRunId, - i + 1L, - layout.positionX(i), + bodies.addBody(layout.positionX(i), layout.positionY(i), layout.positionZ(i)); } - })), "spawn raw benchmark physics bodies"); - long commandApplyNanos = System.nanoTime() - commandStartNanos; - return new BenchmarkSpawnTiming(count, commandApplyNanos, 0L); + }); + return new BenchmarkSpawnTiming(timing.count(), + timing.setupWallNanos(), + timing.physicsStoreApplyNanos(), + 0L); } @Nonnull private static BenchmarkSpawnTiming spawnEntities(@Nonnull Store store, - @Nonnull PhysicsWorldResource resource, + @Nonnull Ref spaceRef, @Nonnull SpaceId spaceId, @Nonnull BenchmarkLayout layout, int count, @@ -187,10 +197,10 @@ private static BenchmarkSpawnTiming spawnEntities(@Nonnull Store st TimeResource time = store.getResource(TimeResource.getResourceType()); PhysicsShapeSpec box = PhysicsShapeSpec.box(0.48f, 0.48f, 0.48f); RigidBodySpawnSettings spawnSettings = RigidBodySpawnSettings.material(0.65f, 0.15f); - ExamplePhysicsUtils.BlockBodyBatchTiming timing = ExamplePhysicsUtils.spawnBlockBodiesMeasured(store, + StressBodyBatches.BlockBodyBatchTiming timing = StressBodyBatches.spawnBlockBodiesMeasured(store, time, - resource, serverTick, + spaceRef, spaceId, count, blockType, @@ -205,14 +215,15 @@ private static BenchmarkSpawnTiming spawnEntities(@Nonnull Store st } }); return new BenchmarkSpawnTiming(timing.count(), - timing.commandApplyNanos(), - timing.entityAttachNanos()); + timing.physicsStoreApplyNanos() + timing.visualAttachNanos(), + timing.physicsStoreApplyNanos(), + timing.visualAttachNanos()); } @Nonnull private String blockType(@Nonnull CommandContext ctx) { return blockTypeArg.provided(ctx) - ? ExamplePhysicsUtils.resolveBlockType(blockTypeArg.get(ctx)) + ? ExampleBlockEntityVisuals.resolveBlockType(blockTypeArg.get(ctx)) : ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE; } @@ -243,8 +254,8 @@ private static Integer tryParseCount(@Nonnull String value) { } private enum BenchmarkMode { - RAW("backend-only raw"), - ENTITY("entity-backed Hytale"); + RAW("PhysicsStore row"), + ENTITY("entity visual"); private final String label; @@ -271,16 +282,14 @@ private record BenchmarkRequest(BenchmarkMode mode, int count, @Nonnull String b } private record BenchmarkSpawnTiming(int spawned, - long commandApplyNanos, - long entityAttachNanos) { + long setupWallNanos, + long physicsStoreApplyNanos, + long visualAttachNanos) { private BenchmarkSpawnTiming { - commandApplyNanos = Math.max(0L, commandApplyNanos); - entityAttachNanos = Math.max(0L, entityAttachNanos); - } - - private long setupWallNanos() { - return commandApplyNanos + entityAttachNanos; + setupWallNanos = Math.max(0L, setupWallNanos); + physicsStoreApplyNanos = Math.max(0L, physicsStoreApplyNanos); + visualAttachNanos = Math.max(0L, visualAttachNanos); } } diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressBodiesCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressBodiesCommand.java index 8316b3ec..f3a01d01 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressBodiesCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressBodiesCommand.java @@ -11,27 +11,28 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodyType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionPrewarmStats; -import dev.hytalemodding.impulse.core.plugin.modules.worldcollision.WorldCollisionMode; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollision; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionPrewarmStats; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsSpaces; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsWorlds; import dev.hytalemodding.impulse.core.plugin.settings.PhysicsBackendExtensionId; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsCollisionLodSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsCollisionLodSettings; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsExtensionSettings; import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSolverSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSpaceSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsVisualMaterializationSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsVisualSyncSettings; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldCollisionSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualMaterializationSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualSyncSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.settings.PhysicsChunkCollisionSettings; import dev.hytalemodding.impulse.core.plugin.settings.PhysicsWorldSettings; -import dev.hytalemodding.impulse.core.plugin.settings.VisualOcclusionMode; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.examples.commands.ExamplePhysicsUtils; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.VisualOcclusionMode; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.examples.utils.ExampleBlockEntityVisuals; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; import java.util.Iterator; import java.util.Locale; import java.util.concurrent.CompletableFuture; @@ -51,7 +52,7 @@ public class StressBodiesCommand extends AbstractAsyncPlayerCommand { DETACHED_VISUAL_DEMATERIALIZATION_RADIUS - DETACHED_VISUAL_MATERIALIZATION_RADIUS; private static final int DETACHED_VISUAL_MAX_MATERIALIZED = 10_000; private static final int DETACHED_VISUAL_MAX_SPAWNS_PER_TICK = 128; - private static final int STRESS_BODY_WORLD_COLLISION_RADIUS = 8; + private static final int STRESS_BODY_CHUNK_COLLISION_RADIUS = 8; private static final PhysicsBackendExtensionId RAPIER_SOLVER_EXTENSION_ID = new PhysicsBackendExtensionId("impulse:rapier_solver"); private static final String RAPIER_INTERNAL_PGS_ITERATIONS = "internalPgsIterations"; @@ -109,7 +110,7 @@ public class StressBodiesCommand extends AbstractAsyncPlayerCommand { ArgTypes.INTEGER); public StressBodiesCommand() { - super("bodies", "Spawn many dynamic box bodies"); + super("bodies", "Spawn dynamic bodies in detached-view, detached, or entity visual modes"); } @Nonnull @@ -119,10 +120,7 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - Vector3d playerPos = ExamplePhysicsUtils.playerPosition(ctx, store, ref); - if (playerPos == null) { - return CompletableFuture.completedFuture(null); - } + Vector3d playerPos = new Vector3d(playerRef.getTransform().getPosition()); StressMode mode = parseMode(ctx); if (mode == null) { @@ -146,25 +144,35 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, if (visualSettings == null) { return CompletableFuture.completedFuture(null); } - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, resource, spaceArg); + SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, world, spaceArg); if (spaceId == null) { return CompletableFuture.completedFuture(null); } - PhysicsSpaceSettings settings = configureStressRuntime(resource, - spaceId, + Ref spaceRef = ExamplePhysicsUtils.resolveSpaceRef(world, spaceId); + if (spaceRef == null) { + ctx.sender().sendMessage(Message.raw("PhysicsStore space id=" + spaceId.value() + + " is not bound.")); + return CompletableFuture.completedFuture(null); + } + Store physicsStore = PhysicsThreading.store(world); + StressRuntimeSettings runtimeSettings = configureStressRuntime(physicsStore, + spaceRef, mode, visibility, visualSettings, collisionLod); + if (runtimeSettings == null) { + ctx.sender().sendMessage(Message.raw("PhysicsStore space id=" + spaceId.value() + + " no longer exists.")); + return CompletableFuture.completedFuture(null); + } TimeResource time = store.getResource(TimeResource.getResourceType()); StressLayout layout = StressLayout.forCount(count, playerPos); long prewarmStartNanos = System.nanoTime(); - int prewarmedSections = prewarmStressWorldCollision(world, - resource, - spaceId, - settings, + int prewarmedSections = prewarmStressTerrain(world, + spaceRef, + runtimeSettings.chunkCollisionSettings(), mode, layout, count); @@ -175,10 +183,10 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, if (mode == StressMode.ENTITY) { PhysicsShapeSpec box = PhysicsShapeSpec.box(0.48f, 0.48f, 0.48f); RigidBodySpawnSettings spawnSettings = RigidBodySpawnSettings.material(0.65f, 0.15f); - ExamplePhysicsUtils.BlockBodyBatchTiming batchTiming = ExamplePhysicsUtils.spawnBlockBodiesMeasured(store, + StressBodyBatches.BlockBodyBatchTiming batchTiming = StressBodyBatches.spawnBlockBodiesMeasured(store, time, - resource, serverTick, + spaceRef, spaceId, count, visualSettings.blockType(), @@ -192,68 +200,59 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, layout.positionZ(i)); } }); - timing = new StressSpawnTiming(batchTiming.commandApplyNanos(), - batchTiming.entityAttachNanos(), - 0L); + timing = new StressSpawnTiming(batchTiming.physicsStoreApplyNanos() + batchTiming.visualAttachNanos(), + batchTiming.physicsStoreApplyNanos(), + batchTiming.visualAttachNanos()); } else { PhysicsShapeSpec box = PhysicsShapeSpec.box(0.48f, 0.48f, 0.48f); RigidBodySpawnSettings spawnSettings = detachedSpawnSettings(collisionPolicy); - long bodyKeyRunId = RigidBodyKey.random().mostSignificantBits(); - long commandStartNanos = System.nanoTime(); - ExamplePhysicsUtils.requireApplied(resource.submitCommands(serverTick, 1, commands -> - commands.spawnBodies(count, + StressBodyBatches.BodyEntityBatchTiming batchTiming = + StressBodyBatches.addDynamicBodyBatchMeasured(world, + spaceRef, spaceId, + count, box, 1.0f, - PhysicsBodyType.DYNAMIC, spawnSettings, - PhysicsBodyKind.BODY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY, - spawns -> { + bodies -> { for (int i = 0; i < count; i++) { - spawns.body(bodyKeyRunId, - i + 1L, - layout.positionX(i), + bodies.addBody(layout.positionX(i), layout.positionY(i), layout.positionZ(i)); } - })), "spawn detached stress bodies"); - timing = new StressSpawnTiming(System.nanoTime() - commandStartNanos, 0L, 0L); - } - if (mode.usesDetachedBodies()) { - long snapshotStartNanos = System.nanoTime(); - resource.refreshBodySnapshots(); - timing = timing.withSnapshotRefreshNanos(System.nanoTime() - snapshotStartNanos); + }); + timing = new StressSpawnTiming(batchTiming.setupWallNanos(), + batchTiming.physicsStoreApplyNanos(), + 0L); } - PhysicsWorldCollisionSettings worldCollisionSettings = - settings.getWorldCollisionSettings(); + PhysicsChunkCollisionSettings chunkCollisionSettings = + runtimeSettings.chunkCollisionSettings(); PhysicsVisualMaterializationSettings visualMaterializationSettings = - settings.getVisualMaterializationSettings(); - PhysicsVisualSyncSettings visualSyncSettings = settings.getVisualSyncSettings(); - PhysicsCollisionLodSettings collisionLodSettings = settings.getCollisionLodSettings(); - PhysicsWorldSettings worldSettings = resource.getWorldSettings(); + runtimeSettings.visualMaterializationSettings(); + PhysicsVisualSyncSettings visualSyncSettings = runtimeSettings.visualSyncSettings(); + PhysicsCollisionLodSettings collisionLodSettings = runtimeSettings.collisionLodSettings(); + PhysicsWorldSettings worldSettings = PhysicsWorlds.settings(physicsStore); - ctx.sender().sendMessage(Message.raw("Spawned " + count + ctx.sender().sendMessage(Message.raw("Added " + count + " stress bodies: setupWallMs=" + millis(prewarmNanos + timing.setupWallNanos()) + " prewarmMs=" + millis(prewarmNanos) - + " commandApplyMs=" + millis(timing.commandApplyNanos()) - + (timing.snapshotRefreshNanos() > 0L - ? " snapshotRefreshMs=" + millis(timing.snapshotRefreshNanos()) - : "") - + (timing.entityAttachNanos() > 0L - ? " entityAttachMs=" + millis(timing.entityAttachNanos()) + + " physicsStoreApplyMs=" + millis(timing.physicsStoreApplyNanos()) + + (timing.visualAttachNanos() > 0L + ? " visualAttachMs=" + millis(timing.visualAttachNanos()) : "") + ": mode=" + mode.serialized() + " space=" + spaceId.value() - + " worldCollision=streaming" - + " bodyCollisionRadius=" + worldCollisionSettings.getWorldCollisionBodyRadius() + + " physicsChunk=streaming" + + " bodyChunkCollisionRadius=" + chunkCollisionSettings.getBodyRadius() + " prewarmedSections=" + prewarmedSections + " step=" + worldSettings.getStepMode().getSerializedName() + "/" + worldSettings.getSimulationSteps() + + " scheduling=" + worldSettings.getStepSchedulingMode().getSerializedName() + " maxStepDt=" + String.format(Locale.ROOT, "%.3f", worldSettings.getMaxStepDt()) + " visuals=" + mode.visualDescription() + (mode == StressMode.ENTITY ? " blockType=" + visualSettings.blockType() : "") + + " " + mode.bindingMessage() + (mode == StressMode.DETACHED_VIEW ? " visualProxyCap=" + visualMaterializationSettings.getDetachedVisualMaxMaterialized() @@ -275,34 +274,46 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, return CompletableFuture.completedFuture(null); } - @Nonnull - private static PhysicsSpaceSettings configureStressRuntime(@Nonnull PhysicsWorldResource resource, - @Nonnull SpaceId spaceId, + @Nullable + private static StressRuntimeSettings configureStressRuntime(@Nonnull Store physicsStore, + @Nonnull Ref spaceRef, @Nonnull StressMode mode, @Nonnull StressVisibility visibility, @Nonnull StressVisualSettings visualSettings, @Nullable Boolean collisionLod) { - PhysicsSpaceSettings settings = new PhysicsSpaceSettings(resource.getSpaceSettings(spaceId)); - PhysicsSolverSettings solverSettings = settings.getSolverSettings(); + PhysicsSolverSettings solverSettings = PhysicsSpaces.solverSettings(physicsStore, spaceRef); + PhysicsExtensionSettings extensionSettings = PhysicsSpaces.extensionSettings(physicsStore, + spaceRef); + PhysicsChunkCollisionSettings chunkCollisionSettings = + PhysicsSpaces.chunkCollisionSettings(physicsStore, spaceRef); + PhysicsCollisionLodSettings collisionLodSettings = + PhysicsSpaces.collisionLodSettings(physicsStore, spaceRef); + PhysicsVisualMaterializationSettings visualMaterializationSettings = + PhysicsSpaces.visualMaterializationSettings(physicsStore, spaceRef); + PhysicsVisualSyncSettings visualSyncSettings = PhysicsSpaces.visualSyncSettings(physicsStore, + spaceRef); + if (solverSettings == null + || extensionSettings == null + || chunkCollisionSettings == null + || collisionLodSettings == null + || visualMaterializationSettings == null + || visualSyncSettings == null) { + return null; + } solverSettings.setSolverIterations(1); solverSettings.setStabilizationIterations(1); - settings.getExtensionSettings().setInt(RAPIER_SOLVER_EXTENSION_ID, + extensionSettings.setInt(RAPIER_SOLVER_EXTENSION_ID, RAPIER_INTERNAL_PGS_ITERATIONS, 1); solverSettings.setDynamicSleepTuning( PhysicsSolverSettings.DEFAULT_DYNAMIC_SLEEP_LINEAR_THRESHOLD, PhysicsSolverSettings.DEFAULT_DYNAMIC_SLEEP_ANGULAR_THRESHOLD, PhysicsSolverSettings.DEFAULT_DYNAMIC_SLEEP_TIME_UNTIL_SLEEP); - PhysicsWorldCollisionSettings worldCollisionSettings = - settings.getWorldCollisionSettings(); - worldCollisionSettings.setWorldCollisionMode(WorldCollisionMode.STREAMING); - worldCollisionSettings.setWorldCollisionBodyRadius( - Math.max(worldCollisionSettings.getWorldCollisionBodyRadius(), - STRESS_BODY_WORLD_COLLISION_RADIUS)); + chunkCollisionSettings.setMode(PhysicsChunkCollisionMode.STREAMING); + chunkCollisionSettings.setBodyRadius( + Math.max(chunkCollisionSettings.getBodyRadius(), + STRESS_BODY_CHUNK_COLLISION_RADIUS)); if (mode.usesDetachedBodies()) { - PhysicsVisualMaterializationSettings visualMaterializationSettings = - settings.getVisualMaterializationSettings(); - PhysicsVisualSyncSettings visualSyncSettings = settings.getVisualSyncSettings(); visualMaterializationSettings.setDetachedVisualMaterializationEnabled( mode == StressMode.DETACHED_VIEW); visualSyncSettings.setVisualVisibilityCullingEnabled(mode == StressMode.DETACHED_VIEW @@ -317,31 +328,41 @@ private static PhysicsSpaceSettings configureStressRuntime(@Nonnull PhysicsWorld visualSyncSettings.setVisualSnapshotPredictionEnabled(visualSettings.predictionEnabled()); visualSyncSettings.setVisualSnapshotSmoothingEnabled(visualSettings.smoothingEnabled()); if (collisionLod != null) { - settings.getCollisionLodSettings().setCollisionLodEnabled(collisionLod); + collisionLodSettings.setCollisionLodEnabled(collisionLod); + } + PhysicsSpaces.putVisualMaterializationSettings(physicsStore, + spaceRef, + visualMaterializationSettings); + PhysicsSpaces.putVisualSyncSettings(physicsStore, spaceRef, visualSyncSettings); + if (collisionLod != null) { + PhysicsSpaces.putCollisionLodSettings(physicsStore, spaceRef, collisionLodSettings); } } - resource.setSpaceSettings(spaceId, settings); - return settings; + PhysicsSpaces.putSolverSettings(physicsStore, spaceRef, solverSettings); + PhysicsSpaces.putExtensionSettings(physicsStore, spaceRef, extensionSettings); + PhysicsSpaces.putChunkCollisionSettings(physicsStore, spaceRef, chunkCollisionSettings); + return new StressRuntimeSettings(chunkCollisionSettings, + visualMaterializationSettings, + visualSyncSettings, + collisionLodSettings); } - private static int prewarmStressWorldCollision(@Nonnull World world, - @Nonnull PhysicsWorldResource resource, - @Nonnull SpaceId spaceId, - @Nonnull PhysicsSpaceSettings settings, + private static int prewarmStressTerrain(@Nonnull World world, + @Nonnull Ref spaceRef, + @Nonnull PhysicsChunkCollisionSettings chunkCollisionSettings, @Nonnull StressMode mode, @Nonnull StressLayout layout, int count) { - PhysicsWorldCollisionSettings worldCollisionSettings = - settings.getWorldCollisionSettings(); if (!mode.usesDetachedBodies() - || worldCollisionSettings.getWorldCollisionMode() != WorldCollisionMode.STREAMING) { + || chunkCollisionSettings.getMode() != PhysicsChunkCollisionMode.STREAMING) { return 0; } - WorldCollisionPrewarmStats stats = resource.ensureWorldCollisionAround(world, - spaceId, + PhysicsChunkCollisionPrewarmStats stats = PhysicsChunkCollision.ensureAround(world, + PhysicsThreading.store(world), + spaceRef, layout.positions(count), - worldCollisionSettings.getWorldCollisionBodyRadius(), + chunkCollisionSettings.getBodyRadius(), 0L); return stats.sectionTargets(); } @@ -475,7 +496,7 @@ private StressVisualSettings parseVisualSettings(@Nonnull CommandContext ctx) { @Nonnull private String blockType(@Nonnull CommandContext ctx) { return blockTypeArg.provided(ctx) - ? ExamplePhysicsUtils.resolveBlockType(blockTypeArg.get(ctx)) + ? ExampleBlockEntityVisuals.resolveBlockType(blockTypeArg.get(ctx)) : ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE; } @@ -530,6 +551,18 @@ private String visualDescription() { }; } + @Nonnull + private String bindingMessage() { + return switch (this) { + case ENTITY -> "Hytale EntityStore block visuals are attached immediately;" + + " physics motion follows copied PhysicsStore snapshots."; + case DETACHED -> "Body-count diagnostics update after PhysicsStore binds the new rows;" + + " no EntityStore visuals are created."; + case DETACHED_VIEW -> "Body-count diagnostics update after PhysicsStore binds the new rows;" + + " detached visual proxies materialize progressively from copied snapshots."; + }; + } + @Nullable private static StressMode from(@Nonnull String value) { return switch (value) { @@ -600,25 +633,21 @@ private record StressVisualSettings(int materializationRadius, boolean smoothingEnabled) { } - private record StressSpawnTiming(long commandApplyNanos, - long entityAttachNanos, - long snapshotRefreshNanos) { + private record StressRuntimeSettings( + @Nonnull PhysicsChunkCollisionSettings chunkCollisionSettings, + @Nonnull PhysicsVisualMaterializationSettings visualMaterializationSettings, + @Nonnull PhysicsVisualSyncSettings visualSyncSettings, + @Nonnull PhysicsCollisionLodSettings collisionLodSettings) { + } - private StressSpawnTiming { - commandApplyNanos = Math.max(0L, commandApplyNanos); - entityAttachNanos = Math.max(0L, entityAttachNanos); - snapshotRefreshNanos = Math.max(0L, snapshotRefreshNanos); - } + private record StressSpawnTiming(long setupWallNanos, + long physicsStoreApplyNanos, + long visualAttachNanos) { - @Nonnull - private StressSpawnTiming withSnapshotRefreshNanos(long snapshotRefreshNanos) { - return new StressSpawnTiming(commandApplyNanos, - entityAttachNanos, - snapshotRefreshNanos); - } - - private long setupWallNanos() { - return commandApplyNanos + entityAttachNanos + snapshotRefreshNanos; + private StressSpawnTiming { + setupWallNanos = Math.max(0L, setupWallNanos); + physicsStoreApplyNanos = Math.max(0L, physicsStoreApplyNanos); + visualAttachNanos = Math.max(0L, visualAttachNanos); } } diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressBodyBatches.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressBodyBatches.java new file mode 100644 index 00000000..3c0d4eeb --- /dev/null +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressBodyBatches.java @@ -0,0 +1,221 @@ +package dev.hytalemodding.impulse.examples.commands.stress; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.modules.time.TimeResource; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodyEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import java.util.function.Consumer; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Quaternionf; +import org.joml.Vector3d; +import org.joml.Vector3f; + +final class StressBodyBatches { + + private StressBodyBatches() { + } + + @Nonnull + static BodyEntityBatchTiming addDynamicBodyBatchMeasured(@Nonnull World world, + @Nonnull Ref spaceRef, + @Nonnull SpaceId spaceId, + int expectedBodies, + @Nonnull PhysicsShapeSpec shape, + float mass, + @Nonnull RigidBodySpawnSettings settings, + @Nonnull Consumer builder) { + DynamicBodyBatchPlan plan = dynamicBodyBatchPlan(spaceRef, + spaceId, + expectedBodies, + shape, + mass, + settings, + builder); + if (plan.isEmpty()) { + return new BodyEntityBatchTiming(0, plan.setupWallNanos(), 0L); + } + + long applyStartNanos = System.nanoTime(); + addPhysicsStoreBodies(PhysicsThreading.store(world), plan.bodies()); + long physicsStoreApplyNanos = System.nanoTime() - applyStartNanos; + return new BodyEntityBatchTiming(plan.count(), + plan.setupWallNanos(), + physicsStoreApplyNanos); + } + + @Nonnull + static BlockBodyBatchTiming spawnBlockBodiesMeasured(@Nonnull Store store, + @Nonnull TimeResource time, + long serverTick, + @Nonnull Ref spaceRef, + @Nonnull SpaceId spaceId, + int expectedBodies, + @Nullable String blockType, + @Nonnull PhysicsShapeSpec shape, + float mass, + @Nonnull RigidBodySpawnSettings settings, + @Nonnull Consumer builder) { + Objects.requireNonNull(spaceRef, "spaceRef"); + Objects.requireNonNull(spaceId, "spaceId"); + Objects.requireNonNull(shape, "shape"); + Objects.requireNonNull(settings, "settings"); + + PhysicsThreading.requireWorldThread(spaceRef.getStore(), + "spawn stress PhysicsStore block body entities"); + if (!spaceRef.isValid()) { + throw new IllegalStateException("Cannot spawn block body batch because the target " + + "PhysicsStore space entity is no longer valid: " + spaceId.value()); + } + + BlockBodyBatchBuilder batch = new BlockBodyBatchBuilder(expectedBodies); + Objects.requireNonNull(builder, "builder").accept(batch); + batch.seal(); + if (batch.isEmpty()) { + return new BlockBodyBatchTiming(0, 0L, 0L); + } + + List> bodyHolders = new ArrayList<>(batch.size()); + for (int i = 0; i < batch.size(); i++) { + UUID bodyUuid = batch.bodyUuid(i); + bodyHolders.add(PhysicsBodyEntities.dynamicBodyHolder(spaceRef, + bodyUuid, + new Vector3f(batch.positionX(i), batch.positionY(i), batch.positionZ(i)), + shape, + mass, + settings, + null)); + } + + long physicsStoreApplyStartNanos = System.nanoTime(); + addPhysicsStoreBodies(spaceRef.getStore(), bodyHolders); + long physicsStoreApplyNanos = System.nanoTime() - physicsStoreApplyStartNanos; + + long visualAttachStartNanos = System.nanoTime(); + for (int i = 0; i < batch.size(); i++) { + UUID bodyUuid = batch.bodyUuid(i); + store.addEntity(ExamplePhysicsUtils.attachedPhysicsBlockEntityHolder(time, + null, + bodyUuid, + blockType, + new Vector3d(batch.positionX(i), batch.positionY(i), batch.positionZ(i)), + new Vector3f(), + new Quaternionf(), + Float.NaN, + mass > 0.0f), + AddReason.SPAWN); + } + long visualAttachNanos = System.nanoTime() - visualAttachStartNanos; + return new BlockBodyBatchTiming(batch.size(), physicsStoreApplyNanos, visualAttachNanos); + } + + @Nonnull + private static DynamicBodyBatchPlan dynamicBodyBatchPlan(@Nonnull Ref spaceRef, + @Nonnull SpaceId spaceId, + int expectedBodies, + @Nonnull PhysicsShapeSpec shape, + float mass, + @Nonnull RigidBodySpawnSettings settings, + @Nonnull Consumer builder) { + Objects.requireNonNull(spaceRef, "spaceRef"); + Objects.requireNonNull(spaceId, "spaceId"); + Objects.requireNonNull(shape, "shape"); + Objects.requireNonNull(settings, "settings"); + + PhysicsThreading.requireWorldThread(spaceRef.getStore(), + "add stress dynamic PhysicsStore body entities"); + if (!spaceRef.isValid()) { + throw new IllegalStateException("Cannot add dynamic body entities because the target " + + "PhysicsStore space entity is no longer valid: " + spaceId.value()); + } + + long setupStartNanos = System.nanoTime(); + BlockBodyBatchBuilder batch = new BlockBodyBatchBuilder(expectedBodies); + Objects.requireNonNull(builder, "builder").accept(batch); + batch.seal(); + if (batch.isEmpty()) { + return new DynamicBodyBatchPlan(List.of(), 0L); + } + + List> bodies = new ArrayList<>(batch.size()); + for (int i = 0; i < batch.size(); i++) { + UUID bodyUuid = batch.bodyUuid(i); + bodies.add(PhysicsBodyEntities.dynamicBodyHolder(spaceRef, + bodyUuid, + new Vector3f(batch.positionX(i), batch.positionY(i), batch.positionZ(i)), + shape, + mass, + settings, + null)); + } + return new DynamicBodyBatchPlan(bodies, System.nanoTime() - setupStartNanos); + } + + private static void addPhysicsStoreBodies(@Nonnull Store store, + @Nonnull Iterable> bodyHolders) { + Objects.requireNonNull(bodyHolders, "bodyHolders"); + PhysicsThreading.requireWorldThread(store, "add stress PhysicsStore body entities"); + List> holders = new ArrayList<>(); + for (Holder holder : bodyHolders) { + holders.add(Objects.requireNonNull(holder, "holder")); + } + if (!holders.isEmpty()) { + @SuppressWarnings("unchecked") + Holder[] holderArray = holders.toArray(Holder[]::new); + store.addEntities(holderArray, AddReason.SPAWN); + } + } + + record BlockBodyBatchTiming(int count, + long physicsStoreApplyNanos, + long visualAttachNanos) { + + BlockBodyBatchTiming { + count = Math.max(0, count); + physicsStoreApplyNanos = Math.max(0L, physicsStoreApplyNanos); + visualAttachNanos = Math.max(0L, visualAttachNanos); + } + } + + record BodyEntityBatchTiming(int count, + long setupWallNanos, + long physicsStoreApplyNanos) { + + BodyEntityBatchTiming { + count = Math.max(0, count); + setupWallNanos = Math.max(0L, setupWallNanos); + physicsStoreApplyNanos = Math.max(0L, physicsStoreApplyNanos); + } + } + + private record DynamicBodyBatchPlan(@Nonnull List> bodies, + long setupWallNanos) { + + DynamicBodyBatchPlan { + bodies = List.copyOf(Objects.requireNonNull(bodies, "bodies")); + setupWallNanos = Math.max(0L, setupWallNanos); + } + + private int count() { + return bodies.size(); + } + + private boolean isEmpty() { + return bodies.isEmpty(); + } + } +} diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressJointsCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressJointsCommand.java index 13f4abac..d15f9b2d 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressJointsCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressJointsCommand.java @@ -11,21 +11,27 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.joint.JointKey; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.JointType; -import dev.hytalemodding.impulse.core.plugin.simulation.recorder.PhysicsCommandRecorder; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.examples.commands.ExamplePhysicsUtils; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsJointEntities; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointType; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import dev.hytalemodding.impulse.examples.utils.ExampleBlockEntityVisuals; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils.CreatedBlockBody; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.joml.Vector3d; +import org.joml.Vector3f; /** - * Builds separate joint rows so backend differences are easier to isolate. + * Builds separate joint entities so backend differences are easier to isolate. * Every row starts with matching body spacing and local anchors, avoiding correction from an * already invalid initial pose. */ @@ -63,115 +69,117 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - Vector3d playerPos = ExamplePhysicsUtils.playerPosition(ctx, store, ref); - if (playerPos == null) { - return CompletableFuture.completedFuture(null); - } + Vector3d playerPos = new Vector3d(playerRef.getTransform().getPosition()); int totalJoints = ExamplePhysicsUtils.optionalInt(ctx, countArg, DEFAULT_JOINTS, 1, MAX_JOINTS); String blockType = blockType(ctx); - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, resource, spaceArg); + SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, world, spaceArg); if (spaceId == null) { return CompletableFuture.completedFuture(null); } TimeResource time = store.getResource(TimeResource.getResourceType()); + Ref spaceRef; + try { + spaceRef = ExamplePhysicsUtils.resolveSpaceRef(world, spaceId); + } catch (IllegalStateException exception) { + spaceRef = null; + } + if (spaceRef == null) { + ctx.sender().sendMessage(Message.raw( + "Cannot queue stress joint demo because the target space is not bound in PhysicsStore.")); + return CompletableFuture.completedFuture(null); + } Vector3d origin = new Vector3d(playerPos).add(-totalJoints * 0.1, 7.0, 5.0); - long serverTick = Math.max(0L, world.getTick()); int createdJoints = 0; - int createdBodies = 0; + int createdBodyCount = 0; + List createdBodyRows = new ArrayList<>(totalJoints + ROWS); int baseJointsPerRow = totalJoints / ROWS; int remainder = totalJoints % ROWS; - for (int row = 0; row < ROWS; row++) { - int rowJoints = baseJointsPerRow + (row < remainder ? 1 : 0); - if (rowJoints <= 0) { - continue; - } + try { + for (int row = 0; row < ROWS; row++) { + int rowJoints = baseJointsPerRow + (row < remainder ? 1 : 0); + if (rowJoints <= 0) { + continue; + } - Vector3d rowOrigin = new Vector3d(origin).add(0.0, 0.0, row * ROW_SPACING); - createdBodies += createRow(store, time, resource, spaceId, rowOrigin, rowJoints, row, - blockType, serverTick); - createdJoints += rowJoints; + Vector3d rowOrigin = new Vector3d(origin).add(0.0, 0.0, row * ROW_SPACING); + createdBodyCount += appendRow(createdBodyRows, + world, + spaceRef, + spaceId, + rowOrigin, + rowJoints, + row, + blockType); + createdJoints += rowJoints; + } + } catch (IllegalStateException exception) { + ctx.sender().sendMessage(Message.raw("Cannot create stress joint demo: " + exception.getMessage())); + return CompletableFuture.completedFuture(null); + } + for (CreatedBlockBody createdBody : createdBodyRows) { + ExamplePhysicsUtils.attachBlockBody(store, time, createdBody); } - ctx.sender().sendMessage(Message.raw("Spawned " + createdJoints - + " stress joints as separate fixed/point/hinge/slider/spring rows with " - + createdBodies + " bodies. blockType=" + blockType + ".")); + ctx.sender().sendMessage(Message.raw("Queued " + createdJoints + + " stress joints across fixed/point/hinge/slider/spring rows with " + + createdBodyCount + " bodies and attached visuals. blockType=" + blockType + ".")); return CompletableFuture.completedFuture(null); } - private static int createRow(@Nonnull Store store, - @Nonnull TimeResource time, - @Nonnull PhysicsWorldResource resource, + private static int appendRow(@Nonnull List createdBodies, + @Nonnull World world, + @Nonnull Ref spaceRef, @Nonnull SpaceId spaceId, @Nonnull Vector3d origin, int jointCount, int jointType, - @Nonnull String blockType, - long serverTick) { + @Nonnull String blockType) { double spacing = jointType == 4 ? TOUCHING_SPACING + SPRING_REST_LENGTH : TOUCHING_SPACING; int bodyCount = jointCount + 1; - RigidBodyKey[] bodyKeys = new RigidBodyKey[bodyCount]; + CreatedBlockBody[] bodies = new CreatedBlockBody[bodyCount]; float[] positions = new float[bodyCount * 3]; - long bodyKeyRunId = RigidBodyKey.random().mostSignificantBits(); - long jointKeyRunId = JointKey.random().mostSignificantBits(); + long bodyUuidRunId = UUID.randomUUID().getMostSignificantBits(); + long jointUuidRunId = UUID.randomUUID().getMostSignificantBits(); PhysicsShapeSpec box = PhysicsShapeSpec.box(HALF_SIZE, HALF_SIZE, HALF_SIZE); RigidBodySpawnSettings spawnSettings = RigidBodySpawnSettings.material(0.65f, 0.1f); for (int i = 0; i < bodyCount; i++) { - RigidBodyKey bodyKey = RigidBodyKey.of(bodyKeyRunId, i + 1L); - bodyKeys[i] = bodyKey; + UUID bodyUuid = new UUID(bodyUuidRunId, i + 1L); int positionOffset = i * 3; positions[positionOffset] = (float) (origin.x + i * spacing); positions[positionOffset + 1] = (float) origin.y; positions[positionOffset + 2] = (float) origin.z; - } - - ExamplePhysicsUtils.requireApplied(resource.submitCommands(serverTick, bodyCount + jointCount, commands -> { - for (int i = 0; i < bodyCount; i++) { - int positionOffset = i * 3; - float mass = i == 0 ? 0.0f : 1.0f; - commands.spawnBody(bodyKeys[i], spawn -> spawn - .space(spaceId) - .shape(box) - .mass(mass) - .dynamic() - .position(positions[positionOffset], + float mass = i == 0 ? 0.0f : 1.0f; + var bodyRef = ExamplePhysicsUtils.addPhysicsStoreBody(world, + ExamplePhysicsUtils.bodyEntity(spaceRef, + bodyUuid, + new Vector3f(positions[positionOffset], positions[positionOffset + 1], - positions[positionOffset + 2]) - .settings(spawnSettings) - .persistent()); - } - - for (int i = 0; i < jointCount; i++) { - RigidBodyKey current = bodyKeys[i + 1]; - createJoint(commands, - JointKey.of(jointKeyRunId, i + 1L), - spaceId, - bodyKeys[i], - current, - jointType); - if (jointType == 1 && i % 5 == 0) { - commands.setBodyVelocity(current, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, true); - } else if (jointType == 4 && i % 3 == 0) { - commands.setBodyVelocity(current, 0.4f, 0.0f, 0.8f, 0.0f, 0.0f, 0.0f, true); - } - } - }), "create stress joint row"); - for (int i = 0; i < bodyCount; i++) { - int positionOffset = i * 3; - ExamplePhysicsUtils.spawnAttachedBlockEntity(store, - time, - bodyKeys[i], + positions[positionOffset + 2]), + box, + mass, + spawnSettings, + initialVelocity(jointType, i))); + CreatedBlockBody created = new CreatedBlockBody( + bodyUuid, + bodyRef, spaceId, blockType, - new Vector3d(positions[positionOffset], - positions[positionOffset + 1], - positions[positionOffset + 2]), + positions[positionOffset], + positions[positionOffset + 1], + positions[positionOffset + 2], i > 0); + bodies[i] = created; + createdBodies.add(created); + } + for (int i = 0; i < jointCount; i++) { + ExamplePhysicsUtils.addJoint(world, + new UUID(jointUuidRunId, i + 1L), + joint(spaceRef, bodies[i], bodies[i + 1], jointType)); } return bodyCount; } @@ -179,15 +187,14 @@ private static int createRow(@Nonnull Store store, @Nonnull private String blockType(@Nonnull CommandContext ctx) { return blockTypeArg.provided(ctx) - ? ExamplePhysicsUtils.resolveBlockType(blockTypeArg.get(ctx)) + ? ExampleBlockEntityVisuals.resolveBlockType(blockTypeArg.get(ctx)) : ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE; } - private static void createJoint(@Nonnull PhysicsCommandRecorder commands, - @Nonnull JointKey jointKey, - @Nonnull SpaceId spaceId, - @Nonnull RigidBodyKey previousKey, - @Nonnull RigidBodyKey currentKey, + @Nonnull + private static JointComponent joint(@Nonnull Ref spaceRef, + @Nonnull CreatedBlockBody previous, + @Nonnull CreatedBlockBody current, int jointType) { JointType type = switch (jointType) { case 0 -> JointType.FIXED; @@ -196,43 +203,53 @@ private static void createJoint(@Nonnull PhysicsCommandRecorder commands, case 3 -> JointType.SLIDER; default -> JointType.SPRING; }; - commands.joint(jointKey, joint -> { - joint.space(spaceId).bodies(previousKey, currentKey); - switch (type) { - case FIXED -> joint.fixed(HALF_SIZE, 0.0f, 0.0f, -HALF_SIZE, 0.0f, 0.0f); - case POINT -> joint.point(HALF_SIZE, 0.0f, 0.0f, -HALF_SIZE, 0.0f, 0.0f); - case HINGE -> joint.hinge(HALF_SIZE, - 0.0f, - 0.0f, - -HALF_SIZE, - 0.0f, - 0.0f, - 0.0f, - 0.0f, - 1.0f) - .limits(-0.8f, 0.8f) - .motor(0.6f, 2.0f); - case SLIDER -> joint.slider(HALF_SIZE, - 0.0f, - 0.0f, - -HALF_SIZE, - 0.0f, - 0.0f, - 1.0f, - 0.0f, - 0.0f) - .limits(-0.35f, 0.35f) - .motor(0.4f, 2.0f); - case SPRING -> joint.spring(HALF_SIZE, - 0.0f, - 0.0f, - -HALF_SIZE, - 0.0f, - 0.0f, - SPRING_REST_LENGTH, - 18.0f, - 2.0f); + JointComponent joint = PhysicsJointEntities.joint(spaceRef, + previous.bodyRef(), + current.bodyRef(), + type, + new Vector3f(HALF_SIZE, 0.0f, 0.0f), + new Vector3f(-HALF_SIZE, 0.0f, 0.0f), + new Vector3f()); + switch (type) { + case FIXED, POINT -> { + } + case HINGE -> { + joint.setAxis(new Vector3f(0.0f, 0.0f, 1.0f)); + joint.setLowerLimit(-0.8f); + joint.setUpperLimit(0.8f); + joint.setMotorEnabled(true); + joint.setMotorTargetVelocity(0.6f); + joint.setMotorMaxForce(2.0f); } - }); + case SLIDER -> { + joint.setAxis(new Vector3f(1.0f, 0.0f, 0.0f)); + joint.setLowerLimit(-0.35f); + joint.setUpperLimit(0.35f); + joint.setMotorEnabled(true); + joint.setMotorTargetVelocity(0.4f); + joint.setMotorMaxForce(2.0f); + } + case SPRING -> { + joint.setSpringRestLength(SPRING_REST_LENGTH); + joint.setSpringStiffness(18.0f); + joint.setSpringDamping(2.0f); + } + } + return joint; + } + + @Nullable + private static Vector3f initialVelocity(int jointType, int bodyIndex) { + if (bodyIndex <= 0) { + return null; + } + int jointIndex = bodyIndex - 1; + if (jointType == 1 && jointIndex % 5 == 0) { + return new Vector3f(0.0f, 0.0f, 1.0f); + } + if (jointType == 4 && jointIndex % 3 == 0) { + return new Vector3f(0.4f, 0.0f, 0.8f); + } + return null; } } diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressRawBodiesCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressRawBodiesCommand.java index 804b11d5..d0077b86 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressRawBodiesCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressRawBodiesCommand.java @@ -10,26 +10,19 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodyType; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyPersistenceMode; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsCommandHandle; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.examples.commands.ExamplePhysicsUtils; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; import java.util.Locale; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; import org.joml.Vector3d; /** - * Creates backend bodies without entity components. - * Compare this with the visible body stress test to separate physics cost from Hytale entity, - * networking, and rendering cost. + * Creates PhysicsStore body rows without EntityStore visual rows. Compare this with the visible + * body stress test to separate physics cost from Hytale entity, networking, and rendering cost. */ public class StressRawBodiesCommand extends AbstractAsyncPlayerCommand { @@ -47,7 +40,7 @@ public class StressRawBodiesCommand extends AbstractAsyncPlayerCommand { ArgTypes.INTEGER); public StressRawBodiesCommand() { - super("raw-bodies", "Spawn physics bodies without Hytale entities"); + super("raw-bodies", "Spawn physics-only PhysicsStore body rows"); } @Nonnull @@ -57,17 +50,19 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - Vector3d playerPos = ExamplePhysicsUtils.playerPosition(ctx, store, ref); - if (playerPos == null) { - return CompletableFuture.completedFuture(null); - } + Vector3d playerPos = new Vector3d(playerRef.getTransform().getPosition()); int count = ExamplePhysicsUtils.optionalInt(ctx, countArg, DEFAULT_COUNT, 1, MAX_COUNT); - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, resource, spaceArg); + SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, world, spaceArg); if (spaceId == null) { return CompletableFuture.completedFuture(null); } + Ref spaceRef = ExamplePhysicsUtils.resolveSpaceRef(world, spaceId); + if (spaceRef == null) { + ctx.sender().sendMessage(Message.raw("PhysicsStore space id=" + spaceId.value() + + " is not bound.")); + return CompletableFuture.completedFuture(null); + } int side = (int) Math.ceil(Math.cbrt(count)); double half = side * SPACING * 0.5; @@ -77,46 +72,27 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, PhysicsShapeSpec box = PhysicsShapeSpec.box(0.48f, 0.48f, 0.48f); RigidBodySpawnSettings spawnSettings = RigidBodySpawnSettings.material(0.65f, 0.15f); - long bodyKeyRunId = RigidBodyKey.random().mostSignificantBits(); - long commandStartNanos = System.nanoTime(); - PhysicsCommandHandle handle = - resource.submitCommands(Math.max(0L, world.getTick()), 1, commands -> - commands.spawnBodies(count, - spaceId, - box, - 1.0f, - PhysicsBodyType.DYNAMIC, - spawnSettings, - PhysicsBodyKind.TEMPORARY, - PhysicsBodyPersistenceMode.RUNTIME_ONLY, - spawns -> { - for (int i = 0; i < count; i++) { - int x = i % side; - int z = (i / side) % side; - int y = i / (side * side); + long totalStartNanos = System.nanoTime(); + StressBodyBatches.BodyEntityBatchTiming timing = StressBodyBatches.addDynamicBodyBatchMeasured(world, + spaceRef, + spaceId, + count, + box, + 1.0f, + spawnSettings, + spawns -> { + for (int i = 0; i < count; i++) { + int x = i % side; + int z = (i / side) % side; + int y = i / (side * side); - spawns.body(bodyKeyRunId, - i + 1L, - (float) (originX + x * SPACING), - (float) (originY + y * SPACING), - (float) (originZ + z * SPACING)); - } - })); - ExamplePhysicsUtils.requireApplied(handle, "spawn raw stress physics bodies"); - PhysicsEventFrame eventFrame = resource.getLatestEventFrame(); - boolean capturedSnapshotIncluded = handle.isIncludedInLatestCapturedSnapshot(eventFrame); - long capturedSnapshotTickLatency = handle.capturedSnapshotServerTickLatency(eventFrame); - long commandApplyNanos = System.nanoTime() - commandStartNanos; - - ctx.sender().sendMessage(Message.raw("Spawned " + count - + " raw physics bodies without entities: commandApplyMs=" - + millis(commandApplyNanos) - + " latestCapturedSnapshotIncluded=" + capturedSnapshotIncluded - + " latestCapturedSnapshotFrame=" + eventFrame.latestCapturedSnapshotFrameEpoch() - + " latestCapturedSnapshotTick=" + eventFrame.latestCapturedSnapshotServerTick() - + " capturedSnapshotTickLatency=" - + (capturedSnapshotIncluded ? Long.toString(capturedSnapshotTickLatency) : "pending") - + ". Use this to separate backend cost from entity/render cost.")); + spawns.addBody((float) (originX + x * SPACING), + (float) (originY + y * SPACING), + (float) (originZ + z * SPACING)); + } + }); + ctx.sender().sendMessage(Message.raw(successMessage(timing, + System.nanoTime() - totalStartNanos))); return CompletableFuture.completedFuture(null); } @@ -124,4 +100,14 @@ private static String millis(long nanos) { return String.format(Locale.ROOT, "%.3f", nanos / 1_000_000.0); } + @Nonnull + private static String successMessage(@Nonnull StressBodyBatches.BodyEntityBatchTiming timing, + long totalWallNanos) { + return "PhysicsStore added body rows for " + timing.count() + + " physics-only bodies: setupWallMs=" + millis(timing.setupWallNanos()) + + " physicsStoreApplyMs=" + millis(timing.physicsStoreApplyNanos()) + + " totalWallMs=" + millis(totalWallNanos) + + ". Body-count updates are visible after PhysicsStore binds the new rows."; + } + } diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressRaycastCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressRaycastCommand.java index f836c3c3..d0fdb37e 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressRaycastCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressRaycastCommand.java @@ -10,11 +10,12 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.query.RaycastClosestBatchQuery; -import dev.hytalemodding.impulse.core.plugin.simulation.RaycastSegment; -import dev.hytalemodding.impulse.examples.commands.ExamplePhysicsUtils; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsAsync; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsRaycasts; +import dev.hytalemodding.impulse.core.plugin.physics.RaycastSegment; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -49,32 +50,33 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - Vector3d playerPos = ExamplePhysicsUtils.playerPosition(ctx, store, ref); - if (playerPos == null) { - return CompletableFuture.completedFuture(null); - } + Vector3d playerPos = new Vector3d(playerRef.getTransform().getPosition()); int rays = ExamplePhysicsUtils.optionalInt(ctx, raysArg, DEFAULT_RAYS, 1, MAX_RAYS); - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, resource, spaceArg); + SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, world, spaceArg); if (spaceId == null) { return CompletableFuture.completedFuture(null); } + Ref spaceRef = ExamplePhysicsUtils.resolveSpaceRef(world, + spaceId); + if (spaceRef == null) { + ctx.sender().sendMessage(Message.raw("PhysicsStore space id=" + spaceId.value() + + " is not bound yet.")); + return CompletableFuture.completedFuture(null); + } int side = (int) Math.ceil(Math.sqrt(rays)); List segments = getRaycastSegments(side, rays, playerPos); long startNanos = System.nanoTime(); - long hits = resource.query(new RaycastClosestBatchQuery(spaceId, segments)) - .completion() - .toCompletableFuture() - .join() - .hitCount(); - long elapsedNanos = System.nanoTime() - startNanos; - - ctx.sender().sendMessage(Message.raw("Ran " + rays + " raycasts: " + hits - + " hits in " + millis(elapsedNanos) + " ms.")); - return CompletableFuture.completedFuture(null); + return PhysicsAsync.acceptOnWorldThread(world, + PhysicsRaycasts.closestBatchAsync(world, spaceRef, segments), + result -> { + long elapsedNanos = System.nanoTime() - startNanos; + ctx.sender().sendMessage(Message.raw("Ran " + rays + " raycasts: " + + result.hitCount() + + " hits in " + millis(elapsedNanos) + " ms.")); + }); } @Nonnull diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressShapesCommand.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressShapesCommand.java index d5daa81b..a2961b06 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressShapesCommand.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/stress/StressShapesCommand.java @@ -1,5 +1,6 @@ package dev.hytalemodding.impulse.examples.commands.stress; +import com.hypixel.hytale.component.AddReason; import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.server.core.Message; @@ -11,15 +12,17 @@ import com.hypixel.hytale.server.core.universe.PlayerRef; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.examples.commands.ExamplePhysicsUtils; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import javax.annotation.Nonnull; +import org.joml.Quaternionf; import org.joml.Vector3d; +import org.joml.Vector3f; public class StressShapesCommand extends AbstractAsyncPlayerCommand { @@ -51,15 +54,13 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, @Nonnull Ref ref, @Nonnull PlayerRef playerRef, @Nonnull World world) { - Vector3d playerPos = ExamplePhysicsUtils.playerPosition(ctx, store, ref); - if (playerPos == null) { - return CompletableFuture.completedFuture(null); - } + Vector3d playerPos = new Vector3d(playerRef.getTransform().getPosition()); int sets = ExamplePhysicsUtils.optionalInt(ctx, setsArg, DEFAULT_SETS, 1, MAX_SETS); - PhysicsWorldResource resource = ExamplePhysicsUtils.resource(store); - SpaceId spaceId = ExamplePhysicsUtils.spaceId(ctx, resource, spaceArg); - if (spaceId == null) { + ExamplePhysicsUtils.SpaceSelection space = ExamplePhysicsUtils.spaceSelection(ctx, + world, + spaceArg); + if (space == null) { return CompletableFuture.completedFuture(null); } TimeResource time = store.getResource(TimeResource.getResourceType()); @@ -71,39 +72,77 @@ protected CompletableFuture executeAsync(@Nonnull CommandContext ctx, int col = set % 4; Vector3d base = new Vector3d(origin).add(col * 7.0, row * 2.2, row * 1.5); - spawn(store, time, resource, spaceId, ShapeType.BOX, axis, + spawn(store, + world, + time, + space.spaceRef(), + ShapeType.BOX, + axis, base, 0.0); - spawn(store, time, resource, spaceId, ShapeType.SPHERE, axis, + spawn(store, + world, + time, + space.spaceRef(), + ShapeType.SPHERE, + axis, base, 1.2); - spawn(store, time, resource, spaceId, ShapeType.CAPSULE, axis, + spawn(store, + world, + time, + space.spaceRef(), + ShapeType.CAPSULE, + axis, base, 2.4); - spawn(store, time, resource, spaceId, ShapeType.CYLINDER, axis, + spawn(store, + world, + time, + space.spaceRef(), + ShapeType.CYLINDER, + axis, base, 3.6); - spawn(store, time, resource, spaceId, ShapeType.CONE, axis, + spawn(store, + world, + time, + space.spaceRef(), + ShapeType.CONE, + axis, base, 4.8); } - ctx.sender().sendMessage(Message.raw("Spawned " + sets + " mixed shape sets (" + ctx.sender().sendMessage(Message.raw("Queued " + sets + " mixed shape sets (" + (sets * 5) + " bodies).")); return CompletableFuture.completedFuture(null); } private static void spawn(@Nonnull Store store, + @Nonnull World world, @Nonnull TimeResource time, - @Nonnull PhysicsWorldResource resource, - @Nonnull SpaceId spaceId, + @Nonnull Ref spaceRef, @Nonnull ShapeType type, @Nonnull PhysicsAxis axis, @Nonnull Vector3d base, double xOffset) { - ExamplePhysicsUtils.spawnBlockBody(store, - time, - resource, - spaceId, - new Vector3d(base).add(xOffset, 0.0, 0.0), + Vector3d position = new Vector3d(base).add(xOffset, 0.0, 0.0); + UUID bodyUuid = UUID.randomUUID(); + var bodyHolder = ExamplePhysicsUtils.bodyEntity(spaceRef, + bodyUuid, + ExamplePhysicsUtils.toVector3f(position), shape(type, axis), 1.0f, - RigidBodySpawnSettings.material(0.6f, 0.25f)); + RigidBodySpawnSettings.material(0.6f, 0.25f), + null); + Ref bodyRef = ExamplePhysicsUtils.addPhysicsStoreBody(world, bodyHolder); + store.addEntity(ExamplePhysicsUtils.attachedPhysicsBlockEntityHolder( + time, + bodyRef, + bodyUuid, + ExamplePhysicsUtils.DEFAULT_BLOCK_TYPE, + position, + new Vector3f(), + new Quaternionf(), + Float.NaN, + true), + AddReason.SPAWN); } @Nonnull diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/events/PhysicsEventSummary.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/events/PhysicsEventSummary.java index 31acc6a4..e14bc952 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/events/PhysicsEventSummary.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/events/PhysicsEventSummary.java @@ -36,9 +36,9 @@ public static String format(@Nonnull PhysicsEventFrame frame) { .append(" space=") .append(firstContact.spaceId().value()) .append(" bodyA=") - .append(firstContact.bodyAKey()) + .append(firstContact.bodyAUuid()) .append(" bodyB=") - .append(firstContact.bodyBKey()) + .append(firstContact.bodyBUuid()) .append(" distance=") .append(formatFloat(firstContact.distance())) .append(" impulse=") diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveBlockPolicy.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveBlockPolicy.java index 02d74b7b..5c1e9906 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveBlockPolicy.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveBlockPolicy.java @@ -4,7 +4,7 @@ import com.hypixel.hytale.protocol.BlockMaterial; import com.hypixel.hytale.server.core.asset.type.blockhitbox.BlockBoundingBoxes; import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; -import dev.hytalemodding.impulse.examples.commands.ExamplePhysicsUtils; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.joml.Vector3f; diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveBlockRuntime.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveBlockRuntime.java index 28a035cf..20899915 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveBlockRuntime.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveBlockRuntime.java @@ -3,6 +3,7 @@ import com.hypixel.hytale.component.AddReason; import com.hypixel.hytale.component.ComponentAccessor; import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; import com.hypixel.hytale.component.Store; import com.hypixel.hytale.math.util.ChunkUtil; import com.hypixel.hytale.math.vector.Rotation3f; @@ -16,20 +17,27 @@ import com.hypixel.hytale.server.core.modules.entity.damage.Damage; import com.hypixel.hytale.server.core.modules.time.TimeResource; import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.chunk.BlockChunk; import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk; +import com.hypixel.hytale.server.core.universe.world.chunk.section.BlockSection; +import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import dev.hytalemodding.impulse.core.plugin.simulation.RigidBodySpawnSettings; -import dev.hytalemodding.impulse.examples.commands.ExamplePhysicsUtils; -import dev.hytalemodding.impulse.examples.commands.ExamplePhysicsUtils.PendingBlockBody; +import dev.hytalemodding.impulse.core.plugin.components.BodyCommandComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollision; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils.CreatedBlockBody; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.function.Consumer; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -70,11 +78,9 @@ public static void scheduleExplosion(@Nonnull Store store, ExplosiveBlockComponent settingsCopy = settings.clone(); world.execute(() -> { TimeResource time = store.getResource(TimeResource.getResourceType()); - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); explode(store, world, time, - resource, spaceId, centerCopy, settingsCopy); @@ -85,15 +91,14 @@ public static void scheduleExplosion(@Nonnull Store store, private static ExplosionResult explode(@Nonnull Store store, @Nonnull World world, @Nonnull TimeResource time, - @Nonnull PhysicsWorldResource resource, @Nonnull SpaceId spaceId, @Nonnull Vector3d center, @Nonnull ExplosiveBlockComponent settings) { return explode(store, + store, holder -> store.addEntity(holder, AddReason.SPAWN), world, time, - resource, spaceId, center, settings); @@ -101,13 +106,20 @@ private static ExplosionResult explode(@Nonnull Store store, @Nonnull private static ExplosionResult explode(@Nonnull ComponentAccessor entityAccessor, + @Nonnull Store store, @Nonnull Consumer> fragmentSpawner, @Nonnull World world, @Nonnull TimeResource time, - @Nonnull PhysicsWorldResource resource, @Nonnull SpaceId spaceId, @Nonnull Vector3d center, @Nonnull ExplosiveBlockComponent settings) { + Ref spaceRef = ExamplePhysicsUtils.resolveSpaceRef(world, + spaceId); + if (spaceRef == null) { + throw new IllegalStateException("Cannot spawn explosive fragments because PhysicsStore " + + "space id=" + spaceId.value() + " is not bound"); + } + ExplosionUtils.performExplosion(DAMAGE_SOURCE, new Vector3d(center), new Rotation3f(), @@ -125,43 +137,57 @@ private static ExplosionResult explode(@Nonnull ComponentAccessor e } List groups = groupFragments(fragments, center, settings.getRadius()); - resource.refreshWorldCollisionAround(world, - spaceId, + Store physicsStore = PhysicsThreading.store(world); + PhysicsChunkCollision.refreshAround(world, + physicsStore, + spaceRef, center, Math.max(8, settings.getRadius() + 4)); - resource.ensureWorldCollisionAround(world, - spaceId, + PhysicsChunkCollision.ensureAround(world, + physicsStore, + spaceRef, groupCenters(groups), Math.max(8, maxGroupCollisionRadius(groups) + 4), Math.max(0L, world.getTick())); - List pending = new ArrayList<>(groups.size()); Vector3f centerF = toVector3f(center); - ExamplePhysicsUtils.requireApplied(resource.submitCommands( - Math.max(0L, world.getTick()), - groups.size() * 2, - commands -> { - for (int i = 0; i < groups.size(); i++) { - FragmentGroup group = groups.get(i); - PendingBlockBody body = ExamplePhysicsUtils.recordBlockBodySpawnAtBodyCenter(commands, - spaceId, - group.center(), - group.blockType(), - group.shape(), - group.mass(), - FRAGMENT_SETTINGS); - pending.add(body); - Vector3f impulse = ExplosiveBlockPolicy.outwardImpulse(centerF, - toVector3f(group.center()), - settings.getImpulseStrength(), - settings.getVerticalLift()) - .mul(group.mass()); - commands.applyBodyImpulse(body.bodyKey(), impulse.x, impulse.y, impulse.z); - } - }), "spawn explosive block fragments"); + List created = new ArrayList<>(groups.size()); + for (FragmentGroup group : groups) { + UUID bodyUuid = UUID.randomUUID(); + Vector3d groupCenter = group.center(); + Vector3f impulse = ExplosiveBlockPolicy.outwardImpulse(centerF, + toVector3f(groupCenter), + settings.getImpulseStrength(), + settings.getVerticalLift()) + .mul(group.mass()); + var bodyRef = ExamplePhysicsUtils.addPhysicsStoreBody(world, + ExamplePhysicsUtils.bodyEntity(spaceRef, + bodyUuid, + toVector3f(groupCenter), + group.shape(), + group.mass(), + FRAGMENT_SETTINGS, + null), + BodyCommandComponent.vector(BodyCommandComponent.Kind.IMPULSE, + impulse.x, + impulse.y, + impulse.z, + false, + 0.0f, + 0.0f, + 0.0f)); + created.add(new CreatedBlockBody(bodyUuid, + bodyRef, + spaceId, + group.blockType(), + (float) groupCenter.x, + (float) groupCenter.y, + (float) groupCenter.z, + group.mass() > 0.0f)); + } for (int i = 0; i < groups.size(); i++) { - spawnGroupVisuals(time, fragmentSpawner, groups.get(i), pending.get(i)); + spawnGroupVisuals(time, fragmentSpawner, groups.get(i), created.get(i)); } return new ExplosionResult(groups.size()); } @@ -169,13 +195,13 @@ private static ExplosionResult explode(@Nonnull ComponentAccessor e private static void spawnGroupVisuals(@Nonnull TimeResource time, @Nonnull Consumer> fragmentSpawner, @Nonnull FragmentGroup group, - @Nonnull PendingBlockBody body) { + @Nonnull CreatedBlockBody body) { boolean controllableAssigned = false; for (FragmentVisual visual : group.visualBlocks()) { boolean controllable = body.controllable() && !controllableAssigned; - Holder holder = ExamplePhysicsUtils.attachedBlockEntityHolder(time, - body.bodyKey(), - body.spaceId(), + Holder holder = ExamplePhysicsUtils.attachedPhysicsBlockEntityHolder(time, + body.bodyRef(), + body.bodyUuid(), visual.blockType(), visual.position(), visual.localPositionOffset(), @@ -221,7 +247,6 @@ private static List collectFragments(@Nonnull World world, } @Nullable - @SuppressWarnings({"deprecation", "removal"}) private static FragmentBlock removeFragmentCandidate(@Nonnull World world, int x, int y, @@ -230,10 +255,15 @@ private static FragmentBlock removeFragmentCandidate(@Nonnull World world, if (chunk == null) { return null; } + BlockChunk blockChunk = loadedBlockChunk(world, x, z); + if (blockChunk == null) { + return null; + } + BlockSection blockSection = loadedBlockSection(world, x, y, z); int localX = chunkBlockCoordinate(x); int localZ = chunkBlockCoordinate(z); - int blockId = chunk.getBlock(localX, y, localZ); - int rotation = chunk.getRotation(localX, y, localZ).index(); + int blockId = blockChunk.getBlock(localX, y, localZ); + int rotation = blockRotationIndex(blockSection, localX, y, localZ); var blockTypeStore = BlockType.getAssetStore(); if (blockTypeStore == null) { return null; @@ -436,7 +466,56 @@ private static int maxGroupCollisionRadius(@Nonnull List groups) @Nullable private static WorldChunk loadedChunk(@Nonnull World world, int x, int z) { - return world.getChunkIfLoaded(ChunkUtil.indexChunkFromBlock(x, z)); + Ref chunkRef = loadedChunkRef(world, x, z); + if (chunkRef == null) { + return null; + } + Store store = world.getChunkStore().getStore(); + return store.getComponentConcurrent(chunkRef, WorldChunk.getComponentType()); + } + + @Nullable + private static BlockChunk loadedBlockChunk(@Nonnull World world, int x, int z) { + Ref chunkRef = loadedChunkRef(world, x, z); + if (chunkRef == null) { + return null; + } + Store store = world.getChunkStore().getStore(); + return store.getComponentConcurrent(chunkRef, BlockChunk.getComponentType()); + } + + @Nullable + private static BlockSection loadedBlockSection(@Nonnull World world, int x, int y, int z) { + if (y < ChunkUtil.MIN_Y || y > ChunkUtil.HEIGHT_MINUS_1) { + return null; + } + ChunkStore chunkStore = world.getChunkStore(); + Ref sectionRef = chunkStore.getChunkSectionReference( + ChunkUtil.chunkCoordinate(x), + ChunkUtil.indexSection(y), + ChunkUtil.chunkCoordinate(z)); + if (sectionRef == null || !sectionRef.isValid()) { + return null; + } + Store store = chunkStore.getStore(); + return store.getComponentConcurrent(sectionRef, BlockSection.getComponentType()); + } + + @Nullable + private static Ref loadedChunkRef(@Nonnull World world, int x, int z) { + Ref chunkRef = world.getChunkStore() + .getChunkReference(ChunkUtil.indexChunkFromBlock(x, z)); + return chunkRef != null && chunkRef.isValid() ? chunkRef : null; + } + + private static int blockRotationIndex(@Nullable BlockSection section, + int localX, + int y, + int localZ) { + if (section == null) { + return 0; + } + return section.getRotationIndex(localX, y, localZ); } @Nonnull @@ -772,6 +851,7 @@ private EntityDamageExplosionConfig(int radius) { 1.0f, null, null, + false, false) }; soundEventId = EXPLOSION_SOUND_EVENT_ID; diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/systems/BenchmarkEntityRemovalDiagnosticsSystem.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/systems/BenchmarkEntityRemovalDiagnosticsSystem.java index 88023e10..f98cf4e9 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/systems/BenchmarkEntityRemovalDiagnosticsSystem.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/systems/BenchmarkEntityRemovalDiagnosticsSystem.java @@ -9,10 +9,11 @@ import com.hypixel.hytale.component.query.Query; import com.hypixel.hytale.component.system.RefSystem; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; @@ -20,13 +21,24 @@ public final class BenchmarkEntityRemovalDiagnosticsSystem extends RefSystem { - private static final ComponentType ATTACHMENT_TYPE = - PhysicsBodyAttachmentComponent.getComponentType(); - private static final Query QUERY = ATTACHMENT_TYPE; - private static final AtomicInteger PHYSICS_ENTITY_REMOVALS = new AtomicInteger(); private static final ConcurrentMap REMOVALS_BY_REASON = new ConcurrentHashMap<>(); + @Nonnull + private final ComponentType attachmentType; + @Nonnull + private final Query query; + + public BenchmarkEntityRemovalDiagnosticsSystem() { + this(BodyAttachmentComponent.getComponentType()); + } + + BenchmarkEntityRemovalDiagnosticsSystem( + @Nonnull ComponentType attachmentType) { + this.attachmentType = Objects.requireNonNull(attachmentType, "attachmentType"); + this.query = attachmentType; + } + public static void reset() { PHYSICS_ENTITY_REMOVALS.set(0); REMOVALS_BY_REASON.clear(); @@ -55,7 +67,7 @@ public void onEntityRemove(@Nonnull Ref ref, @Nonnull RemoveReason reason, @Nonnull Store store, @Nonnull CommandBuffer commandBuffer) { - PhysicsBodyAttachmentComponent component = store.getComponent(ref, ATTACHMENT_TYPE); + BodyAttachmentComponent component = store.getComponent(ref, attachmentType); if (component == null) { return; } @@ -68,6 +80,6 @@ public void onEntityRemove(@Nonnull Ref ref, @Nonnull @Override public Query getQuery() { - return QUERY; + return query; } } diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/systems/ExplosiveFuseContactSystem.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/systems/ExplosiveFuseContactSystem.java index 9b7fdaa3..4e38af92 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/systems/ExplosiveFuseContactSystem.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/systems/ExplosiveFuseContactSystem.java @@ -6,18 +6,19 @@ import com.hypixel.hytale.component.Store; import com.hypixel.hytale.component.system.WorldEventSystem; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; import dev.hytalemodding.impulse.api.PhysicsContactPhase; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyKind; -import dev.hytalemodding.impulse.core.plugin.body.PhysicsBodyRegistrationView; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsEntityAttachments; import dev.hytalemodding.impulse.core.plugin.events.PhysicsContactEvent; import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFramePublishedEvent; import dev.hytalemodding.impulse.core.plugin.events.PhysicsFrameEvent; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollision; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; import dev.hytalemodding.impulse.examples.explosive.ExplosiveBlockComponent; import dev.hytalemodding.impulse.examples.explosive.ExplosiveBlockRuntime; import dev.hytalemodding.impulse.examples.explosive.ExplosiveFuseComponent; +import java.util.UUID; import javax.annotation.Nonnull; import org.joml.Vector3d; import org.joml.Vector3f; @@ -29,8 +30,6 @@ public final class ExplosiveFuseContactSystem ExplosiveBlockComponent.getComponentType(); private static final ComponentType FUSE_TYPE = ExplosiveFuseComponent.getComponentType(); - private static final ComponentType ATTACHMENT_TYPE = - PhysicsBodyAttachmentComponent.getComponentType(); public ExplosiveFuseContactSystem() { super(PhysicsEventFramePublishedEvent.class); @@ -40,44 +39,53 @@ public ExplosiveFuseContactSystem() { public void handle(@Nonnull Store store, @Nonnull CommandBuffer commandBuffer, @Nonnull PhysicsEventFramePublishedEvent event) { - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); + Store physicsStore = + PhysicsThreading.store(store.getExternalData().getWorld()); long tick = Math.max(0L, store.getExternalData().getWorld().getTick()); + ComponentType attachmentType = + BodyAttachmentComponent.getComponentType(); for (PhysicsFrameEvent frameEvent : event.frame().physicsEvents()) { if (frameEvent instanceof PhysicsContactEvent contact && contact.phase() != PhysicsContactPhase.ENDED) { armIfExplosiveTouchesWorld(commandBuffer, - resource, + store, + physicsStore, tick, - contact.bodyAKey(), - contact.bodyBKey(), + attachmentType, + contact.bodyAUuid(), + contact.bodyBUuid(), contactCenter(contact.pointOnB())); armIfExplosiveTouchesWorld(commandBuffer, - resource, + store, + physicsStore, tick, - contact.bodyBKey(), - contact.bodyAKey(), + attachmentType, + contact.bodyBUuid(), + contact.bodyAUuid(), contactCenter(contact.pointOnA())); } } } private static void armIfExplosiveTouchesWorld(@Nonnull CommandBuffer commandBuffer, - @Nonnull PhysicsWorldResource resource, + @Nonnull Store store, + @Nonnull Store physicsStore, long tick, - @Nonnull RigidBodyKey explosiveBodyKey, - @Nonnull RigidBodyKey otherBodyKey, + @Nonnull ComponentType attachmentType, + @Nonnull UUID explosiveBodyUuid, + @Nonnull UUID otherBodyUuid, @Nonnull Vector3d explosionCenter) { - if (!isWorldCollision(resource, otherBodyKey)) { + if (!isTerrain(physicsStore, otherBodyUuid)) { return; } - for (Ref ref : resource.getBodyAttachments(explosiveBodyKey)) { - PhysicsBodyAttachmentComponent attachment = commandBuffer.getComponent(ref, ATTACHMENT_TYPE); + for (Ref ref : PhysicsEntityAttachments.attachments(store, explosiveBodyUuid)) { + BodyAttachmentComponent attachment = commandBuffer.getComponent(ref, attachmentType); ExplosiveBlockComponent explosive = commandBuffer.getComponent(ref, EXPLOSIVE_TYPE); ExplosiveFuseComponent fuse = commandBuffer.getComponent(ref, FUSE_TYPE); if (attachment == null || explosive == null || fuse == null - || !explosiveBodyKey.equals(attachment.getBodyKey())) { + || !explosiveBodyUuid.equals(attachment.getBodyUuid())) { continue; } ExplosiveFuseComponent updated = fuse.clone(); @@ -87,10 +95,9 @@ private static void armIfExplosiveTouchesWorld(@Nonnull CommandBuffer physicsStore, + @Nonnull UUID bodyUuid) { + return PhysicsChunkCollision.isChunkCollisionBody(physicsStore, bodyUuid); } @Nonnull diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/systems/ExplosiveFuseTickSystem.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/systems/ExplosiveFuseTickSystem.java index 8b88e058..8a6befe4 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/systems/ExplosiveFuseTickSystem.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/systems/ExplosiveFuseTickSystem.java @@ -12,17 +12,21 @@ import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; import com.hypixel.hytale.server.core.universe.world.World; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.resources.PhysicsWorldResource; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.snapshots.PhysicsBodySnapshot; import dev.hytalemodding.impulse.examples.explosive.ExplosiveBlockComponent; import dev.hytalemodding.impulse.examples.explosive.ExplosiveBlockRuntime; import dev.hytalemodding.impulse.examples.explosive.ExplosiveFuseComponent; +import java.util.Objects; +import java.util.UUID; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.joml.Vector3d; +import org.joml.Vector3f; public final class ExplosiveFuseTickSystem extends EntityTickingSystem implements QuerySystem { @@ -31,12 +35,26 @@ public final class ExplosiveFuseTickSystem extends EntityTickingSystem FUSE_TYPE = ExplosiveFuseComponent.getComponentType(); - private static final ComponentType ATTACHMENT_TYPE = - PhysicsBodyAttachmentComponent.getComponentType(); private static final ComponentType TRANSFORM_TYPE = TransformComponent.getComponentType(); - private static final Query QUERY = - Query.and(EXPLOSIVE_TYPE, FUSE_TYPE, ATTACHMENT_TYPE, TRANSFORM_TYPE); + + @Nonnull + private final ComponentType attachmentType; + @Nonnull + private final Query query; + + public ExplosiveFuseTickSystem() { + this(BodyAttachmentComponent.getComponentType()); + } + + ExplosiveFuseTickSystem( + @Nonnull ComponentType attachmentType) { + this.attachmentType = Objects.requireNonNull(attachmentType, "attachmentType"); + this.query = Query.and(EXPLOSIVE_TYPE, + FUSE_TYPE, + this.attachmentType, + TRANSFORM_TYPE); + } @Override public boolean isParallel(int archetypeChunkSize, int taskCount) { @@ -55,16 +73,15 @@ public void tick(float dt, return; } ExplosiveBlockComponent explosive = chunk.getComponent(index, EXPLOSIVE_TYPE); - PhysicsBodyAttachmentComponent attachment = chunk.getComponent(index, ATTACHMENT_TYPE); + BodyAttachmentComponent attachment = chunk.getComponent(index, attachmentType); TransformComponent transform = chunk.getComponent(index, TRANSFORM_TYPE); - SpaceId spaceId = attachment != null ? attachment.getSpaceId() : null; + SpaceId spaceId = attachment != null ? attachmentSpaceId(store, attachment) : null; if (explosive == null || attachment == null || transform == null || spaceId == null) { return; } Ref ref = chunk.getReferenceTo(index); - PhysicsWorldResource resource = store.getResource(PhysicsWorldResource.getResourceType()); - PhysicsBodySnapshot snapshot = bodySnapshot(resource, attachment.getBodyKey()); + BodyMotionSnapshot snapshot = bodySnapshot(store, attachment); Vector3d currentCenter = explosionCenter(snapshot, transform); if (!fuse.isArmed()) { ExplosiveFuseComponent updated = fuse.clone(); @@ -86,13 +103,14 @@ public void tick(float dt, spaceId, center, explosive); + destroySourceBody(world, attachment); commandBuffer.removeEntity(ref, RemoveReason.REMOVE); } @Nonnull @Override public Query getQuery() { - return QUERY; + return query; } private static long currentTick(@Nonnull Store store) { @@ -100,7 +118,7 @@ private static long currentTick(@Nonnull Store store) { } @Nonnull - private static Vector3d explosionCenter(@Nullable PhysicsBodySnapshot snapshot, + private static Vector3d explosionCenter(@Nullable BodyMotionSnapshot snapshot, @Nonnull TransformComponent transform) { if (snapshot != null) { return ExplosiveBlockRuntime.sourceExplosionCenter(new Vector3d(snapshot.positionX(), @@ -111,15 +129,58 @@ private static Vector3d explosionCenter(@Nullable PhysicsBodySnapshot snapshot, } @Nullable - private static PhysicsBodySnapshot bodySnapshot(@Nonnull PhysicsWorldResource resource, - @Nonnull RigidBodyKey bodyKey) { - if (resource.getBodyRegistrationView(bodyKey) != null) { - try { - return resource.getBodySnapshot(bodyKey); - } catch (IllegalArgumentException ignored) { - // Fall back to the last synced entity transform if the source body was destroyed. - } + private static SpaceId attachmentSpaceId(@Nonnull Store store, + @Nonnull BodyAttachmentComponent attachment) { + Store physics = PhysicsThreading.store(store.getExternalData().getWorld()); + Ref bodyRef = attachment.getBodyRef(); + SpaceId spaceId = bodyRef != null && bodyRef.isValid() + ? PhysicsBodies.spaceId(physics, bodyRef) + : null; + if (spaceId == null) { + spaceId = PhysicsBodies.spaceId(physics, attachment.getBodyUuid()); + } + return spaceId; + } + + @Nullable + private static BodyMotionSnapshot bodySnapshot(@Nonnull Store store, + @Nonnull BodyAttachmentComponent attachment) { + UUID bodyUuid = attachment.getBodyUuid(); + Store physics = PhysicsThreading.store(store.getExternalData().getWorld()); + Ref bodyRef = attachment.getBodyRef(); + PhysicsBodySnapshot snapshot = bodyRef != null && bodyRef.isValid() + ? PhysicsBodies.snapshot(physics, bodyRef) + : null; + if (snapshot != null && !bodyUuid.equals(snapshot.bodyUuid())) { + snapshot = null; + } + if (snapshot == null) { + snapshot = PhysicsBodies.snapshot(physics, bodyUuid); + } + return snapshot != null ? BodyMotionSnapshot.from(snapshot) : null; + } + + private static void destroySourceBody(@Nonnull World world, + @Nonnull BodyAttachmentComponent attachment) { + Ref bodyRef = attachment.getBodyRef(); + if (bodyRef != null && bodyRef.isValid()) { + PhysicsBodies.destroyAsync(world, bodyRef); + return; } - return null; + PhysicsBodies.destroyAsync(world, attachment.getBodyUuid()); + } + + private record BodyMotionSnapshot(float positionX, + float positionY, + float positionZ, + float linearVelocityY) { + + @Nonnull + private static BodyMotionSnapshot from(@Nonnull PhysicsBodySnapshot snapshot) { + Vector3f position = snapshot.position(); + Vector3f velocity = snapshot.linearVelocity(); + return new BodyMotionSnapshot(position.x, position.y, position.z, velocity.y); + } + } } diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ExampleBlockEntityVisuals.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/utils/ExampleBlockEntityVisuals.java similarity index 85% rename from impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ExampleBlockEntityVisuals.java rename to impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/utils/ExampleBlockEntityVisuals.java index 637fc7c3..225291cf 100644 --- a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/commands/ExampleBlockEntityVisuals.java +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/utils/ExampleBlockEntityVisuals.java @@ -1,4 +1,4 @@ -package dev.hytalemodding.impulse.examples.commands; +package dev.hytalemodding.impulse.examples.utils; import com.hypixel.hytale.component.ComponentType; import com.hypixel.hytale.component.Holder; @@ -7,12 +7,12 @@ import com.hypixel.hytale.server.core.modules.physics.component.Velocity; import com.hypixel.hytale.server.core.modules.time.TimeResource; import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.core.plugin.settings.PhysicsVisualMaterializationSettings; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualMaterializationSettings; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.joml.Vector3d; -final class ExampleBlockEntityVisuals { +public final class ExampleBlockEntityVisuals { private ExampleBlockEntityVisuals() { } @@ -38,7 +38,7 @@ static void stripHytaleRuntimeComponents(@Nonnull Holder holder, } @Nonnull - static String resolveBlockType(@Nullable String blockType) { + public static String resolveBlockType(@Nullable String blockType) { return blockType == null || blockType.isBlank() ? PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_BLOCK_TYPE : blockType.trim(); diff --git a/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/utils/ExamplePhysicsUtils.java b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/utils/ExamplePhysicsUtils.java new file mode 100644 index 00000000..fe0894fb --- /dev/null +++ b/impulse-examples/src/main/java/dev/hytalemodding/impulse/examples/utils/ExamplePhysicsUtils.java @@ -0,0 +1,361 @@ +package dev.hytalemodding.impulse.examples.utils; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.command.system.CommandContext; +import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg; +import com.hypixel.hytale.server.core.modules.time.TimeResource; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import dev.hytalemodding.impulse.api.SpaceId; +import dev.hytalemodding.impulse.core.plugin.components.BodyCommandComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.builtin.control.ImpulseControllableComponent; +import dev.hytalemodding.impulse.builtin.control.PhysicsControlSessions; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.PhysicsEntityAttachments; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.components.BodyAttachmentComponent; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodies; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsBodyEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsEntities; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsSpaces; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsThreading; +import dev.hytalemodding.impulse.core.plugin.modules.physicsentity.settings.PhysicsVisualMaterializationSettings; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsShapeSpec; +import dev.hytalemodding.impulse.core.plugin.physics.RigidBodySpawnSettings; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.UUID; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.joml.Quaternionf; +import org.joml.Vector3d; +import org.joml.Vector3f; + +public final class ExamplePhysicsUtils { + + public static final String DEFAULT_BLOCK_TYPE = + PhysicsVisualMaterializationSettings.DEFAULT_DETACHED_VISUAL_BLOCK_TYPE; + + private ExamplePhysicsUtils() { + } + + @Nullable + public static Ref resolveSpaceRef(@Nonnull World world, + @Nonnull SpaceId spaceId) { + Store store = PhysicsThreading.store(world); + return PhysicsSpaces.resolveRef(store, spaceId); + } + + @Nullable + public static SpaceSelection spaceSelection(@Nonnull CommandContext ctx, + @Nonnull World world, + @Nonnull OptionalArg spaceArg) { + Store store = PhysicsThreading.store(world); + if (spaceArg.provided(ctx)) { + int rawSpaceId = spaceArg.get(ctx); + if (rawSpaceId <= 0) { + ctx.sender().sendMessage(Message.raw("Space id must be a positive integer.")); + return null; + } + SpaceId spaceId = new SpaceId(rawSpaceId); + Ref spaceRef = PhysicsSpaces.resolveRef(store, spaceId); + if (spaceRef != null) { + return new SpaceSelection(spaceId, spaceRef); + } + if (PhysicsSpaces.hasSpace(store, spaceId)) { + ctx.sender().sendMessage(Message.raw("PhysicsStore space id=" + rawSpaceId + + " is not bound yet.")); + } else { + ctx.sender().sendMessage(Message.raw("No physics space id=" + rawSpaceId + " exists.")); + } + return null; + } + + SpaceId firstSpaceId = PhysicsSpaces.spaceIds(store) + .stream() + .min(Comparator.comparingInt(SpaceId::value)) + .orElse(null); + if (firstSpaceId == null) { + ctx.sender().sendMessage(Message.raw("No physics space exists. Run " + + "`/impulse space create --backend=` before running Impulse example commands.")); + return null; + } + Ref spaceRef = PhysicsSpaces.resolveRef(store, firstSpaceId); + if (spaceRef == null) { + ctx.sender().sendMessage(Message.raw("PhysicsStore space id=" + firstSpaceId.value() + + " is not bound yet.")); + return null; + } + return new SpaceSelection(firstSpaceId, spaceRef); + } + + @Nonnull + public static Ref addPhysicsStoreBody(@Nonnull World world, + @Nonnull Holder holder) { + Store store = PhysicsThreading.store(world); + return addPhysicsStoreBody(store, holder); + } + + @Nonnull + public static Ref addPhysicsStoreBody(@Nonnull World world, + @Nonnull Holder holder, + @Nonnull BodyCommandComponent command) { + Store store = PhysicsThreading.store(world); + Ref bodyRef = addPhysicsStoreBody(store, holder); + PhysicsBodies.appendCommand(store, bodyRef, command); + return bodyRef; + } + + @Nonnull + private static Ref addPhysicsStoreBody(@Nonnull Store store, + @Nonnull Holder holder) { + PhysicsThreading.requireWorldThread(store, "add a PhysicsStore body entity"); + return store.addEntity(Objects.requireNonNull(holder, "holder"), AddReason.SPAWN); + } + + @Nonnull + public static Ref addJoint(@Nonnull World world, + @Nonnull UUID jointUuid, + @Nonnull JointComponent joint) { + Store store = PhysicsThreading.store(world); + PhysicsThreading.requireWorldThread(store, "add a PhysicsStore joint entity"); + return store.addEntity(PhysicsEntities.jointHolder(store, + Objects.requireNonNull(jointUuid, "jointUuid"), + joint), AddReason.SPAWN); + } + + @Nullable + public static SpaceId spaceId(@Nonnull CommandContext ctx, + @Nonnull World world, + @Nonnull OptionalArg spaceArg) { + Store store = PhysicsThreading.store(world); + if (spaceArg.provided(ctx)) { + int rawSpaceId = spaceArg.get(ctx); + if (rawSpaceId <= 0) { + ctx.sender().sendMessage(Message.raw("Space id must be a positive integer.")); + return null; + } + SpaceId spaceId = new SpaceId(rawSpaceId); + if (!PhysicsSpaces.hasSpace(store, spaceId)) { + ctx.sender().sendMessage(Message.raw("No physics space id=" + rawSpaceId + " exists.")); + return null; + } + return spaceId; + } + + SpaceId firstSpaceId = PhysicsSpaces.spaceIds(store) + .stream() + .min(Comparator.comparingInt(SpaceId::value)) + .orElse(null); + if (firstSpaceId == null) { + ctx.sender().sendMessage(Message.raw("No physics space exists. Run " + + "`/impulse space create --backend=` before running Impulse example commands.")); + } + return firstSpaceId; + } + + @Nonnull + public static Holder bodyEntity(@Nonnull Ref spaceRef, + @Nonnull UUID bodyUuid, + @Nonnull Vector3f bodyCenter, + @Nonnull PhysicsShapeSpec shape, + float mass, + @Nonnull RigidBodySpawnSettings settings, + @Nullable Vector3f linearVelocity) { + return PhysicsBodyEntities.dynamicBodyHolder(spaceRef, + bodyUuid, + bodyCenter, + shape, + mass, + settings, + linearVelocity); + } + + public static void attachBlockBody(@Nonnull Store store, + @Nonnull TimeResource time, + @Nonnull CreatedBlockBody created) { + Ref bodyRef = created.bodyRef(); + PhysicsThreading.requireWorldThread(bodyRef.getStore(), + "attach a visual to a created PhysicsStore body entity"); + if (!bodyRef.isValid()) { + throw new IllegalStateException("Cannot attach visual because PhysicsStore body entity " + + "is no longer valid: " + created.bodyUuid()); + } + spawnAttachedBlockEntity(store, + time, + bodyRef, + created.bodyUuid(), + created.blockType(), + new Vector3d(created.positionX(), created.positionY(), created.positionZ()), + created.visualOriginOffsetY(), + created.controllable()); + } + + @Nullable + public static Ref spawnExternalBodyViewBlockEntity(@Nonnull Store store, + @Nonnull TimeResource time, + @Nullable Ref bodyRef, + @Nonnull UUID bodyUuid, + @Nonnull Vector3d visualPosition, + @Nullable String blockType) { + requirePhysicsEntityVisuals(); + + Holder holder = + ExampleBlockEntityVisuals.impulseOwnedBlockVisual(time, blockType, visualPosition); + + holder.addComponent(BodyAttachmentComponent.getComponentType(), + externalBodyAttachment(bodyUuid, bodyRef)); + return store.addEntity(holder, AddReason.SPAWN); + } + + @Nullable + private static Ref spawnAttachedBlockEntity(@Nonnull Store store, + @Nonnull TimeResource time, + @Nullable Ref bodyRef, + @Nonnull UUID physicsBodyUuid, + @Nullable String blockType, + @Nonnull Vector3d visualPosition, + float visualOriginOffsetY, + boolean controllable) { + Holder holder = attachedPhysicsBlockEntityHolder(time, + bodyRef, + physicsBodyUuid, + blockType, + visualPosition, + new Vector3f(), + new Quaternionf(), + visualOriginOffsetY, + controllable); + return store.addEntity(holder, AddReason.SPAWN); + } + + @Nonnull + public static Holder attachedPhysicsBlockEntityHolder(@Nonnull TimeResource time, + @Nullable Ref bodyRef, + @Nonnull UUID physicsBodyUuid, + @Nullable String blockType, + @Nonnull Vector3d visualPosition, + @Nonnull Vector3f localPositionOffset, + @Nonnull Quaternionf localRotationOffset, + float visualOriginOffsetY, + boolean controllable) { + requirePhysicsEntityVisuals(); + Holder holder = blockEntityHolder(time, blockType, visualPosition); + holder.addComponent(BodyAttachmentComponent.getComponentType(), + impulseOwnedBodyAttachment(physicsBodyUuid, + bodyRef, + localPositionOffset, + localRotationOffset, + visualOriginOffsetY)); + if (controllable && PhysicsControlSessions.isAvailable()) { + holder.addComponent(ImpulseControllableComponent.getComponentType(), + new ImpulseControllableComponent()); + } + return holder; + } + + @Nonnull + static BodyAttachmentComponent externalBodyAttachment(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef) { + BodyAttachmentComponent attachment = BodyAttachmentComponent.externalEntity(bodyUuid); + attachment.setBodyRef(bodyRef); + return attachment; + } + + @Nonnull + static BodyAttachmentComponent impulseOwnedBodyAttachment(@Nonnull UUID bodyUuid, + @Nullable Ref bodyRef, + @Nonnull Vector3f localPositionOffset, + @Nonnull Quaternionf localRotationOffset, + float visualOriginOffsetY) { + BodyAttachmentComponent attachment = BodyAttachmentComponent.impulseOwnedVisual(bodyUuid, + localPositionOffset, + localRotationOffset, + visualOriginOffsetY); + attachment.setBodyRef(bodyRef); + return attachment; + } + + @Nonnull + private static Holder blockEntityHolder(@Nonnull TimeResource time, + @Nullable String blockType, + @Nonnull Vector3d visualPosition) { + return ExampleBlockEntityVisuals.impulseOwnedBlockVisual(time, blockType, visualPosition); + } + + private static void requirePhysicsEntityVisuals() { + if (!PhysicsEntityAttachments.isAvailable()) { + throw new IllegalStateException( + "Impulse PhysicsEntity integration is not available. " + + "Enable HytaleModding:ImpulsePhysicsEntity to spawn entity-backed example visuals."); + } + } + + public static int optionalInt(@Nonnull CommandContext ctx, + @Nonnull OptionalArg arg, + int defaultValue, + int min, + int max) { + int value = arg.provided(ctx) ? arg.get(ctx) : defaultValue; + if (value < min) { + return min; + } + return Math.min(value, max); + } + + public static Vector3f toVector3f(@Nonnull Vector3d vector) { + return new Vector3f((float) vector.x, (float) vector.y, (float) vector.z); + } + + public record SpaceSelection(@Nonnull SpaceId spaceId, + @Nonnull Ref spaceRef) { + + public SpaceSelection { + Objects.requireNonNull(spaceId, "spaceId"); + Objects.requireNonNull(spaceRef, "spaceRef"); + } + } + + public record CreatedBlockBody(@Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef, + @Nonnull SpaceId spaceId, + @Nullable String blockType, + float positionX, + float positionY, + float positionZ, + boolean controllable, + float visualOriginOffsetY) { + + public CreatedBlockBody(@Nonnull UUID bodyUuid, + @Nonnull Ref bodyRef, + @Nonnull SpaceId spaceId, + @Nullable String blockType, + float positionX, + float positionY, + float positionZ, + boolean controllable) { + this(bodyUuid, + bodyRef, + spaceId, + blockType, + positionX, + positionY, + positionZ, + controllable, + Float.NaN); + } + + public CreatedBlockBody { + Objects.requireNonNull(bodyUuid, "bodyUuid"); + Objects.requireNonNull(bodyRef, "bodyRef"); + Objects.requireNonNull(spaceId, "spaceId"); + } + } + +} diff --git a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/ImpulseExamplesManifestTest.java b/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/ImpulseExamplesManifestTest.java new file mode 100644 index 00000000..b50e5012 --- /dev/null +++ b/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/ImpulseExamplesManifestTest.java @@ -0,0 +1,42 @@ +package dev.hytalemodding.impulse.examples; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.codec.ExtraInfo; +import com.hypixel.hytale.codec.util.RawJsonReader; +import com.hypixel.hytale.common.plugin.PluginIdentifier; +import com.hypixel.hytale.common.plugin.PluginManifest; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class ImpulseExamplesManifestTest { + + @Test + void generatedManifestDependsOnBundledControlSubPlugin() throws IOException { + PluginManifest manifest = decodeGeneratedManifest(); + + assertTrue(manifest.getDependencies() + .containsKey(new PluginIdentifier("HytaleModding", "Impulse"))); + assertTrue(manifest.getDependencies() + .containsKey(new PluginIdentifier("HytaleModding", "ImpulsePhysicsEntity"))); + assertTrue(manifest.getDependencies() + .containsKey(new PluginIdentifier("HytaleModding", "ImpulsePhysicsChunk"))); + assertTrue(manifest.getDependencies() + .containsKey(new PluginIdentifier("HytaleModding", "ImpulseControl"))); + } + + private static PluginManifest decodeGeneratedManifest() throws IOException { + InputStream stream = ImpulseExamplesManifestTest.class + .getClassLoader() + .getResourceAsStream("manifest.json"); + assertNotNull(stream); + try (InputStreamReader input = new InputStreamReader(stream, StandardCharsets.UTF_8); + RawJsonReader reader = new RawJsonReader(input, RawJsonReader.READ_BUFFER.get())) { + return PluginManifest.CODEC.decodeJson(reader, new ExtraInfo()); + } + } +} diff --git a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/commands/DirectionalPendulumsCommandTest.java b/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/commands/DirectionalPendulumsCommandTest.java new file mode 100644 index 00000000..3887a787 --- /dev/null +++ b/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/commands/DirectionalPendulumsCommandTest.java @@ -0,0 +1,586 @@ +package dev.hytalemodding.impulse.examples.commands; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.hypixel.hytale.component.ComponentRegistry; +import com.hypixel.hytale.component.ComponentRegistryProxy; +import com.hypixel.hytale.component.EmptyResourceStorage; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.storage.PhysicsStore; +import com.hypixel.hytale.server.core.util.thread.TickingThread; +import dev.hytalemodding.impulse.api.BackendId; +import dev.hytalemodding.impulse.api.ImpulseBackendRegistry; +import dev.hytalemodding.impulse.api.PhysicsCollisionFilters; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider; +import dev.hytalemodding.impulse.api.testsupport.FakePhysicsBackendRuntimeProvider.FakePhysicsBackendRuntime; +import dev.hytalemodding.impulse.core.internal.PhysicsComponentTypeRegistry; +import dev.hytalemodding.impulse.core.internal.resources.BackendSpaceHandle; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsResourceTypes; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRestoreStatusResource; +import dev.hytalemodding.impulse.core.internal.resources.PhysicsRuntimeResource; +import dev.hytalemodding.impulse.core.internal.systems.IdentityIndexSystem; +import dev.hytalemodding.impulse.core.internal.systems.PersistenceHydrationSystem; +import dev.hytalemodding.impulse.core.internal.systems.SpaceSettingsApplicationSystem; +import dev.hytalemodding.impulse.core.internal.systems.binding.SpaceBindingSystem; +import dev.hytalemodding.impulse.core.plugin.components.CollisionFilterComponent; +import dev.hytalemodding.impulse.core.plugin.components.DynamicsComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointComponent; +import dev.hytalemodding.impulse.core.plugin.components.JointType; +import dev.hytalemodding.impulse.core.plugin.components.SpaceComponent; +import dev.hytalemodding.impulse.core.plugin.components.TargetComponent; +import dev.hytalemodding.impulse.core.plugin.modules.physicschunk.PhysicsChunkCollisionMode; +import dev.hytalemodding.impulse.core.plugin.physics.PhysicsSpaces; +import dev.hytalemodding.impulse.core.plugin.settings.PhysicsSolverSettings; +import dev.hytalemodding.impulse.examples.utils.ExamplePhysicsUtils.CreatedBlockBody; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import javax.annotation.Nonnull; +import org.joml.Vector3d; +import org.joml.Vector3f; +import org.junit.jupiter.api.Test; +import org.objenesis.ObjenesisStd; + +class DirectionalPendulumsCommandTest { + + private static final ObjenesisStd OBJENESIS = new ObjenesisStd(); + private static final BackendId JOLT_BACKEND_ID = + new BackendId("test:directional-pendulums-jolt"); + private static final BackendId RAPIER_BACKEND_ID = + new BackendId("test:directional-pendulums-rapier"); + private static final BackendId BINDING_BACKEND_ID = + new BackendId("test:directional-pendulums-binding"); + private static final String CINEMATIC_JOLT_BLOCK_TYPE = "Rock_Aqua"; + private static final String CINEMATIC_RAPIER_BLOCK_TYPE = "Rock_Basalt"; + private static final float CINEMATIC_MIN_LINEAR_SPEED = 2.0f; + private static final float CINEMATIC_MIN_ANGULAR_SPEED = 1.0f; + + @Test + void spawnDemoCreatesFourNonStreamingSpacesWithDirectionalDoublePendulums() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(world("directional-pendulums-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + + DirectionalPendulumsCommand.SpawnResult result = + DirectionalPendulumsCommand.spawnDemo(store, + JOLT_BACKEND_ID, + RAPIER_BACKEND_ID, + new Vector3d(10.0, 20.0, 30.0)); + + assertEquals(4, result.pendulums().size()); + assertEquals(12, result.createdBodies().size()); + + Vector3f[] expectedGravities = { + new Vector3f(0.0f, -9.81f, 0.0f), + new Vector3f(0.0f, 9.81f, 0.0f), + new Vector3f(9.81f, 0.0f, 0.0f), + new Vector3f(-9.81f, 0.0f, 0.0f) + }; + Vector3f[] expectedAnchors = { + new Vector3f(2.0f, 20.0f, 22.0f), + new Vector3f(18.0f, 20.0f, 22.0f), + new Vector3f(2.0f, 20.0f, 38.0f), + new Vector3f(18.0f, 20.0f, 38.0f) + }; + BackendId[] expectedBackends = { + JOLT_BACKEND_ID, + JOLT_BACKEND_ID, + RAPIER_BACKEND_ID, + RAPIER_BACKEND_ID + }; + for (int index = 0; index < expectedGravities.length; index++) { + DirectionalPendulumsCommand.CreatedPendulum pendulum = + result.pendulums().get(index); + assertEquals(expectedBackends[index], pendulum.backendId()); + assertVectorEquals(expectedGravities[index], pendulum.gravity()); + assertBodyPosition(expectedAnchors[index], pendulum.anchor()); + assertPendulumVisual(pendulum.anchor()); + assertPendulumVisual(pendulum.upper()); + assertPendulumVisual(pendulum.lower()); + assertEquals(PhysicsChunkCollisionMode.NONE, + PhysicsSpaces.chunkCollisionSettings(store, pendulum.spaceRef()).getMode()); + assertNoSleepSolverSettings(store, pendulum.spaceRef()); + assertVectorEquals(expectedGravities[index], + store.getComponent(pendulum.spaceRef(), SpaceComponent.getComponentType()) + .getGravity()); + assertBodyMass(store, pendulum.anchor().bodyRef(), 0.0f); + assertBodyMass(store, pendulum.upper().bodyRef(), 1.0f); + assertBodyMass(store, pendulum.lower().bodyRef(), 1.0f); + assertPendulumCollisionFilter(store, pendulum.anchor().bodyRef()); + assertPendulumCollisionFilter(store, pendulum.upper().bodyRef()); + assertPendulumCollisionFilter(store, pendulum.lower().bodyRef()); + assertTangentialVelocity(store, + pendulum.upper().bodyRef(), + expectedGravities[index]); + assertTangentialVelocity(store, + pendulum.lower().bodyRef(), + expectedGravities[index]); + assertPointJoint(store, + pendulum.topJointRef(), + pendulum.anchor().bodyRef(), + pendulum.upper().bodyRef(), + new Vector3f(), + normalized(expectedGravities[index], -1.0f)); + assertEndpointVelocityMatch(store, + pendulum.anchor().bodyRef(), + new Vector3f(), + pendulum.upper().bodyRef(), + normalized(expectedGravities[index], -1.0f)); + assertPointJoint(store, + pendulum.middleJointRef(), + pendulum.upper().bodyRef(), + pendulum.lower().bodyRef(), + normalized(expectedGravities[index], 1.0f), + normalized(expectedGravities[index], -1.0f)); + assertEndpointVelocityMatch(store, + pendulum.upper().bodyRef(), + normalized(expectedGravities[index], 1.0f), + pendulum.lower().bodyRef(), + normalized(expectedGravities[index], -1.0f)); + } + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void spawnCinematicPresetKeepsFourNonStreamingBackendSplitSpaces() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(world("directional-pendulums-cinematic-spaces-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + + DirectionalPendulumsCommand.SpawnResult result = + spawnCinematicDemo(store, + JOLT_BACKEND_ID, + RAPIER_BACKEND_ID, + new Vector3d(10.0, 20.0, 30.0)); + + assertEquals(4, result.pendulums().size()); + assertEquals(12, result.createdBodies().size()); + BackendId[] expectedBackends = { + JOLT_BACKEND_ID, + JOLT_BACKEND_ID, + RAPIER_BACKEND_ID, + RAPIER_BACKEND_ID + }; + Vector3f[] expectedGravities = directionalGravities(); + for (int index = 0; index < expectedBackends.length; index++) { + DirectionalPendulumsCommand.CreatedPendulum pendulum = + result.pendulums().get(index); + assertEquals(expectedBackends[index], pendulum.backendId()); + assertVectorEquals(expectedGravities[index], pendulum.gravity()); + assertEquals(PhysicsChunkCollisionMode.NONE, + PhysicsSpaces.chunkCollisionSettings(store, pendulum.spaceRef()).getMode()); + assertNoSleepSolverSettings(store, pendulum.spaceRef()); + assertVectorEquals(expectedGravities[index], + store.getComponent(pendulum.spaceRef(), SpaceComponent.getComponentType()) + .getGravity()); + } + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void spawnCinematicPresetUsesTighterBackendDistinctVisualLayout() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(world("directional-pendulums-cinematic-layout-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + + DirectionalPendulumsCommand.SpawnResult result = + spawnCinematicDemo(store, + JOLT_BACKEND_ID, + RAPIER_BACKEND_ID, + new Vector3d(10.0, 20.0, 30.0)); + + Vector3f[] expectedAnchors = { + new Vector3f(6.0f, 20.0f, 26.0f), + new Vector3f(14.0f, 20.0f, 26.0f), + new Vector3f(6.0f, 20.0f, 34.0f), + new Vector3f(14.0f, 20.0f, 34.0f) + }; + String[] expectedBlockTypes = { + CINEMATIC_JOLT_BLOCK_TYPE, + CINEMATIC_JOLT_BLOCK_TYPE, + CINEMATIC_RAPIER_BLOCK_TYPE, + CINEMATIC_RAPIER_BLOCK_TYPE + }; + for (int index = 0; index < expectedAnchors.length; index++) { + DirectionalPendulumsCommand.CreatedPendulum pendulum = + result.pendulums().get(index); + assertBodyPosition(expectedAnchors[index], pendulum.anchor()); + assertCinematicVisual(pendulum.anchor(), expectedBlockTypes[index]); + assertCinematicVisual(pendulum.upper(), expectedBlockTypes[index]); + assertCinematicVisual(pendulum.lower(), expectedBlockTypes[index]); + } + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void spawnCinematicPresetUsesStrongerLateralInitialMotion() { + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + Store store = registry.addStore( + new PhysicsStore(world("directional-pendulums-cinematic-motion-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + + DirectionalPendulumsCommand.SpawnResult result = + spawnCinematicDemo(store, + JOLT_BACKEND_ID, + RAPIER_BACKEND_ID, + new Vector3d(10.0, 20.0, 30.0)); + + Vector3f[] expectedGravities = directionalGravities(); + for (int index = 0; index < expectedGravities.length; index++) { + DirectionalPendulumsCommand.CreatedPendulum pendulum = + result.pendulums().get(index); + assertStrongLateralVelocity(store, + pendulum.upper().bodyRef(), + expectedGravities[index]); + assertStrongLateralVelocity(store, + pendulum.lower().bodyRef(), + expectedGravities[index]); + assertEndpointVelocityMatch(store, + pendulum.anchor().bodyRef(), + new Vector3f(), + pendulum.upper().bodyRef(), + normalized(expectedGravities[index], -1.0f)); + assertEndpointVelocityMatch(store, + pendulum.upper().bodyRef(), + normalized(expectedGravities[index], 1.0f), + pendulum.lower().bodyRef(), + normalized(expectedGravities[index], -1.0f)); + } + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + @Test + void storeTickAppliesDirectionalSpaceGravitiesWithoutChangingThem() { + FakePhysicsBackendRuntimeProvider provider = + new FakePhysicsBackendRuntimeProvider(BINDING_BACKEND_ID, false, false); + ImpulseBackendRegistry.registerRuntimeProvider(provider); + ComponentRegistry registry = new ComponentRegistry<>(); + ComponentRegistryProxy proxy = + new ComponentRegistryProxy<>(new ArrayList<>(), registry); + PhysicsComponentTypeRegistry.registerComponentTypes(proxy); + PhysicsResourceTypes.registerResourceTypes(proxy); + proxy.registerSystem(new PersistenceHydrationSystem()); + proxy.registerSystem(new IdentityIndexSystem()); + proxy.registerSystem(new SpaceBindingSystem()); + proxy.registerSystem(new SpaceSettingsApplicationSystem()); + Store store = registry.addStore( + new PhysicsStore(world("directional-pendulums-binding-test")), + EmptyResourceStorage.get()); + try { + markCurrentThreadAsWorldThread(store); + PhysicsRestoreStatusResource restore = store.getResource( + PhysicsRestoreStatusResource.getResourceType()); + restore.markComplete(); + restore.markHydrated(); + + DirectionalPendulumsCommand.SpawnResult result = + DirectionalPendulumsCommand.spawnDemo(store, + BINDING_BACKEND_ID, + new Vector3d(10.0, 20.0, 30.0)); + + store.tick(0.0f); + assertFalse(restore.isFailed(), restore.getFailureMessage()); + assertEquals(1, provider.createdRuntimes().size()); + FakePhysicsBackendRuntime backendRuntime = provider.createdRuntimes().get(0); + assertBackendGravities(store, backendRuntime, result); + + store.tick(0.0f); + assertFalse(restore.isFailed(), restore.getFailureMessage()); + assertBackendGravities(store, backendRuntime, result); + } finally { + registry.removeStore(store); + registry.shutdown(); + } + } + + private static void assertBackendGravities(@Nonnull Store store, + @Nonnull FakePhysicsBackendRuntime backendRuntime, + @Nonnull DirectionalPendulumsCommand.SpawnResult result) { + PhysicsRuntimeResource runtime = store.getResource( + PhysicsRuntimeResource.getResourceType()); + for (DirectionalPendulumsCommand.CreatedPendulum pendulum : result.pendulums()) { + BackendSpaceHandle handle = runtime.getSpaceHandle(pendulum.spaceRef()); + assertNotNull(handle); + assertVectorEquals(pendulum.gravity(), + backendGravity(backendRuntime, handle.value())); + } + } + + @Nonnull + @SuppressWarnings({"unchecked", "rawtypes"}) + private static DirectionalPendulumsCommand.SpawnResult spawnCinematicDemo( + @Nonnull Store store, + @Nonnull BackendId joltBackendId, + @Nonnull BackendId rapierBackendId, + @Nonnull Vector3d center) { + try { + Class presetType = Class.forName( + DirectionalPendulumsCommand.class.getName() + "$Preset"); + Object cinematicPreset = Enum.valueOf((Class) presetType.asSubclass(Enum.class), + "CINEMATIC"); + Method spawnDemo = DirectionalPendulumsCommand.class.getDeclaredMethod("spawnDemo", + Store.class, + BackendId.class, + BackendId.class, + Vector3d.class, + presetType); + spawnDemo.setAccessible(true); + return (DirectionalPendulumsCommand.SpawnResult) spawnDemo.invoke(null, + store, + joltBackendId, + rapierBackendId, + center, + cinematicPreset); + } catch (ClassNotFoundException exception) { + throw new AssertionError("Expected DirectionalPendulumsCommand.Preset.CINEMATIC", + exception); + } catch (NoSuchMethodException exception) { + throw new AssertionError("Expected cinematic spawnDemo overload", exception); + } catch (IllegalAccessException exception) { + throw new AssertionError("Could not invoke cinematic spawnDemo overload", exception); + } catch (InvocationTargetException exception) { + Throwable cause = exception.getTargetException(); + if (cause instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (cause instanceof Error error) { + throw error; + } + throw new AssertionError("Cinematic spawnDemo overload failed", cause); + } + } + + @Nonnull + private static Vector3f backendGravity(@Nonnull FakePhysicsBackendRuntime runtime, + int spaceId) { + Vector3f gravity = new Vector3f(); + runtime.getGravity(spaceId, gravity::set); + return gravity; + } + + private static void assertBodyMass(@Nonnull Store store, + @Nonnull Ref bodyRef, + float expectedMass) { + DynamicsComponent dynamics = store.getComponent(bodyRef, + DynamicsComponent.getComponentType()); + assertNotNull(dynamics); + assertEquals(expectedMass, dynamics.getMass()); + } + + private static void assertPendulumCollisionFilter(@Nonnull Store store, + @Nonnull Ref bodyRef) { + CollisionFilterComponent filter = store.getComponent(bodyRef, + CollisionFilterComponent.getComponentType()); + assertNotNull(filter); + assertEquals(PhysicsCollisionFilters.DYNAMIC_BODY, filter.getCollisionGroup()); + assertEquals(PhysicsCollisionFilters.TERRAIN, filter.getCollisionMask()); + } + + private static void assertPointJoint(@Nonnull Store store, + @Nonnull Ref jointRef, + @Nonnull Ref expectedBodyARef, + @Nonnull Ref expectedBodyBRef, + @Nonnull Vector3f expectedAnchorA, + @Nonnull Vector3f expectedAnchorB) { + JointComponent joint = store.getComponent(jointRef, + JointComponent.getComponentType()); + assertNotNull(joint); + assertEquals(JointType.POINT, joint.getType()); + assertSame(expectedBodyARef, joint.getBodyARef()); + assertSame(expectedBodyBRef, joint.getBodyBRef()); + assertVectorEquals(expectedAnchorA, joint.getAnchorA()); + assertVectorEquals(expectedAnchorB, joint.getAnchorB()); + } + + private static void assertBodyPosition(@Nonnull Vector3f expected, + @Nonnull CreatedBlockBody body) { + assertEquals(expected.x, body.positionX(), 0.0001f); + assertEquals(expected.y, body.positionY(), 0.0001f); + assertEquals(expected.z, body.positionZ(), 0.0001f); + } + + private static void assertPendulumVisual(@Nonnull CreatedBlockBody body) { + assertEquals(DirectionalPendulumsCommand.PENDULUM_BLOCK_TYPE, body.blockType()); + assertEquals(DirectionalPendulumsCommand.PENDULUM_VISUAL_ORIGIN_OFFSET_Y, + body.visualOriginOffsetY(), + 0.0001f); + } + + private static void assertCinematicVisual(@Nonnull CreatedBlockBody body, + @Nonnull String expectedBlockType) { + assertEquals(expectedBlockType, body.blockType()); + assertEquals(DirectionalPendulumsCommand.PENDULUM_VISUAL_ORIGIN_OFFSET_Y, + body.visualOriginOffsetY(), + 0.0001f); + } + + private static void assertNoSleepSolverSettings(@Nonnull Store store, + @Nonnull Ref spaceRef) { + PhysicsSolverSettings settings = PhysicsSpaces.solverSettings(store, spaceRef); + assertNotNull(settings); + assertEquals(DirectionalPendulumsCommand.PENDULUM_SLEEP_LINEAR_THRESHOLD, + settings.getDynamicSleepLinearThreshold(), + 0.0001f); + assertEquals(DirectionalPendulumsCommand.PENDULUM_SLEEP_ANGULAR_THRESHOLD, + settings.getDynamicSleepAngularThreshold(), + 0.0001f); + assertEquals(DirectionalPendulumsCommand.PENDULUM_TIME_UNTIL_SLEEP, + settings.getDynamicSleepTimeUntilSleep(), + 0.0001f); + } + + private static void assertTangentialVelocity(@Nonnull Store store, + @Nonnull Ref bodyRef, + @Nonnull Vector3f gravity) { + TargetComponent target = store.getComponent(bodyRef, + TargetComponent.getComponentType()); + assertNotNull(target); + Vector3f velocity = target.getLinearVelocity(); + assertEquals(0.0f, velocity.dot(gravity), 0.0001f); + if (velocity.lengthSquared() <= 0.1f) { + throw new AssertionError("Expected tangential initial velocity but got " + velocity); + } + Vector3f angularVelocity = target.getAngularVelocity(); + assertEquals(0.0f, angularVelocity.dot(gravity), 0.0001f); + if (angularVelocity.lengthSquared() <= 0.1f) { + throw new AssertionError("Expected angular initial velocity but got " + + angularVelocity); + } + } + + private static void assertStrongLateralVelocity(@Nonnull Store store, + @Nonnull Ref bodyRef, + @Nonnull Vector3f gravity) { + TargetComponent target = store.getComponent(bodyRef, + TargetComponent.getComponentType()); + assertNotNull(target); + Vector3f velocity = target.getLinearVelocity(); + assertEquals(0.0f, velocity.dot(gravity), 0.0001f); + assertTrue(velocity.length() >= CINEMATIC_MIN_LINEAR_SPEED, + () -> "Expected cinematic linear speed >= " + CINEMATIC_MIN_LINEAR_SPEED + + " but got " + velocity); + Vector3f angularVelocity = target.getAngularVelocity(); + assertEquals(0.0f, angularVelocity.dot(gravity), 0.0001f); + assertTrue(angularVelocity.length() >= CINEMATIC_MIN_ANGULAR_SPEED, + () -> "Expected cinematic angular speed >= " + CINEMATIC_MIN_ANGULAR_SPEED + + " but got " + angularVelocity); + } + + private static void assertEndpointVelocityMatch(@Nonnull Store store, + @Nonnull Ref bodyARef, + @Nonnull Vector3f anchorA, + @Nonnull Ref bodyBRef, + @Nonnull Vector3f anchorB) { + assertVectorEquals(endpointVelocity(store, bodyARef, anchorA), + endpointVelocity(store, bodyBRef, anchorB)); + } + + @Nonnull + private static Vector3f endpointVelocity(@Nonnull Store store, + @Nonnull Ref bodyRef, + @Nonnull Vector3f localAnchor) { + TargetComponent target = store.getComponent(bodyRef, + TargetComponent.getComponentType()); + if (target == null) { + return new Vector3f(); + } + Vector3f angular = target.getAngularVelocity(); + Vector3f anchorVelocity = new Vector3f(); + angular.cross(localAnchor, anchorVelocity); + return target.getLinearVelocity().add(anchorVelocity); + } + + private static void assertVectorEquals(@Nonnull Vector3f expected, + @Nonnull Vector3f actual) { + assertEquals(expected.x, actual.x, 0.0001f); + assertEquals(expected.y, actual.y, 0.0001f); + assertEquals(expected.z, actual.z, 0.0001f); + } + + @Nonnull + private static Vector3f[] directionalGravities() { + return new Vector3f[] { + new Vector3f(0.0f, -9.81f, 0.0f), + new Vector3f(0.0f, 9.81f, 0.0f), + new Vector3f(9.81f, 0.0f, 0.0f), + new Vector3f(-9.81f, 0.0f, 0.0f) + }; + } + + @Nonnull + private static Vector3f normalized(@Nonnull Vector3f vector, float scale) { + return new Vector3f(vector).normalize().mul(scale); + } + + @Nonnull + private static World world(@Nonnull String worldName) { + World world = OBJENESIS.newInstance(World.class); + try { + Field name = World.class.getDeclaredField("name"); + name.setAccessible(true); + name.set(world, worldName); + return world; + } catch (ReflectiveOperationException exception) { + throw new AssertionError("Could not create test world", exception); + } + } + + private static void markCurrentThreadAsWorldThread(@Nonnull Store store) { + try { + Method setThread = TickingThread.class.getDeclaredMethod("setThread", Thread.class); + setThread.setAccessible(true); + setThread.invoke(store.getExternalData().getWorld(), Thread.currentThread()); + } catch (NoSuchMethodException | IllegalAccessException exception) { + throw new AssertionError("Could not mark test world thread", exception); + } catch (InvocationTargetException exception) { + throw new AssertionError("Could not mark test world thread", + exception.getTargetException()); + } + } +} diff --git a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/commands/EventsCommandTest.java b/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/commands/EventsCommandTest.java deleted file mode 100644 index 787a9578..00000000 --- a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/commands/EventsCommandTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package dev.hytalemodding.impulse.examples.commands; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.PhysicsContactPhase; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsContactEvent; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import dev.hytalemodding.impulse.examples.events.PhysicsEventSummary; -import java.util.List; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class EventsCommandTest { - - @Test - void formatsLatestContactEventSummary() { - RigidBodyKey first = RigidBodyKey.of(1L, 2L); - RigidBodyKey second = RigidBodyKey.of(3L, 4L); - PhysicsEventFrame frame = new PhysicsEventFrame(12L, - 34L, - 56L, - 78L, - 90L, - 11L, - List.of(), - List.of(), - List.of(), - List.of(new PhysicsContactEvent(new SpaceId(5), - PhysicsContactPhase.OBSERVED, - first, - second, - new Vector3f(1.0f, 2.0f, 3.0f), - new Vector3f(4.0f, 5.0f, 6.0f), - new Vector3f(0.0f, 1.0f, 0.0f), - -0.125f, - 2.5f)), - 1); - - String summary = PhysicsEventSummary.format(frame); - - assertTrue(summary.contains("contacts=1")); - assertTrue(summary.contains("firstContact=observed")); - assertTrue(summary.contains("space=5")); - assertTrue(summary.contains("bodyA=" + first)); - assertTrue(summary.contains("bodyB=" + second)); - assertTrue(summary.contains("impulse=2.500")); - assertTrue(summary.contains("droppedBackendEvents=1")); - } -} diff --git a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/commands/ExampleBlockEntityVisualsTest.java b/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/commands/ExampleBlockEntityVisualsTest.java deleted file mode 100644 index 14fa2fe2..00000000 --- a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/commands/ExampleBlockEntityVisualsTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package dev.hytalemodding.impulse.examples.commands; - -import static org.junit.jupiter.api.Assertions.assertFalse; - -import com.hypixel.hytale.component.ComponentRegistry; -import com.hypixel.hytale.component.ComponentType; -import com.hypixel.hytale.component.Holder; -import com.hypixel.hytale.server.core.modules.entity.DespawnComponent; -import com.hypixel.hytale.server.core.modules.physics.component.Velocity; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import org.junit.jupiter.api.Test; - -class ExampleBlockEntityVisualsTest { - - @Test - void impulseOwnedBlockVisualsDoNotKeepHytaleVelocity() { - ComponentRegistry registry = new ComponentRegistry<>(); - ComponentType despawnType = - registry.registerComponent(DespawnComponent.class, DespawnComponent::new); - ComponentType velocityType = - registry.registerComponent(Velocity.class, Velocity::new); - Holder holder = registry.newHolder(); - holder.addComponent(despawnType, new DespawnComponent()); - holder.addComponent(velocityType, new Velocity()); - - ExampleBlockEntityVisuals.stripHytaleRuntimeComponents(holder, despawnType, velocityType); - - assertFalse(holder.getArchetype().contains(despawnType)); - assertFalse(holder.getArchetype().contains(velocityType)); - } -} diff --git a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/commands/ExamplePhysicsUtilsTest.java b/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/commands/ExamplePhysicsUtilsTest.java deleted file mode 100644 index 1457a86b..00000000 --- a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/commands/ExamplePhysicsUtilsTest.java +++ /dev/null @@ -1,166 +0,0 @@ -package dev.hytalemodding.impulse.examples.commands; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.hypixel.hytale.component.ComponentRegistry; -import com.hypixel.hytale.component.Holder; -import com.hypixel.hytale.server.core.modules.entity.EntityModule; -import com.hypixel.hytale.server.core.modules.entity.component.HeadRotation; -import com.hypixel.hytale.server.core.modules.entity.component.ModelComponent; -import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; -import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.core.ImpulsePlugin; -import dev.hytalemodding.impulse.core.internal.modules.control.ControlLifecycle; -import dev.hytalemodding.impulse.core.internal.modules.control.components.PhysicsControlSessionComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyAttachmentComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyCollisionComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyDynamicsComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyIdentityComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyKinematicTargetComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyMaterialComponent; -import dev.hytalemodding.impulse.core.plugin.components.PhysicsBodyShapeComponent; -import dev.hytalemodding.impulse.core.plugin.modules.control.ImpulseControllableComponent; -import dev.hytalemodding.impulse.core.plugin.simulation.PhysicsShapeSpec; -import java.lang.reflect.Field; -import javax.annotation.Nonnull; -import org.joml.Vector3d; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -class ExamplePhysicsUtilsTest { - - private ComponentRegistry registry; - private Object previousEntityModule; - private Object previousImpulsePlugin; - - @BeforeEach - void registerComponentTypes() throws Exception { - previousEntityModule = staticField(EntityModule.class, "instance").get(null); - previousImpulsePlugin = staticField(ImpulsePlugin.class, "instance").get(null); - registry = new ComponentRegistry<>(); - registerEntityModuleTypes(); - registerImpulsePluginTypes(); - ControlLifecycle.enable(); - ImpulseControllableComponent.setComponentType(registry.registerComponent( - ImpulseControllableComponent.class, - "ImpulseControllable", - ImpulseControllableComponent.CODEC)); - PhysicsControlSessionComponent.setComponentType(registry.registerComponent( - PhysicsControlSessionComponent.class, - PhysicsControlSessionComponent::new)); - } - - @AfterEach - void clearComponentTypes() throws Exception { - ControlLifecycle.disable(); - ImpulseControllableComponent.clearComponentType(); - PhysicsControlSessionComponent.clearComponentType(); - staticField(EntityModule.class, "instance").set(null, previousEntityModule); - staticField(ImpulsePlugin.class, "instance").set(null, previousImpulsePlugin); - registry.shutdown(); - } - - private void registerEntityModuleTypes() throws Exception { - EntityModule entityModule = allocate(EntityModule.class); - setField(entityModule, - "transformComponentType", - registry.registerComponent(TransformComponent.class, - "Transform", - TransformComponent.CODEC)); - setField(entityModule, - "headRotationComponentType", - registry.registerComponent(HeadRotation.class, - "HeadRotation", - HeadRotation.CODEC)); - setField(entityModule, - "modelComponentType", - registry.registerComponent(ModelComponent.class, () -> new ModelComponent(null))); - staticField(EntityModule.class, "instance").set(null, entityModule); - } - - private void registerImpulsePluginTypes() throws Exception { - ImpulsePlugin plugin = allocate(ImpulsePlugin.class); - setField(plugin, - "physicsBodyAttachmentComponentType", - registry.registerComponent(PhysicsBodyAttachmentComponent.class, - "PhysicsBodyAttachment", - PhysicsBodyAttachmentComponent.CODEC)); - setField(plugin, - "physicsBodyIdentityComponentType", - registry.registerComponent(PhysicsBodyIdentityComponent.class, - "PhysicsBodyIdentity", - PhysicsBodyIdentityComponent.CODEC)); - setField(plugin, - "physicsBodyShapeComponentType", - registry.registerComponent(PhysicsBodyShapeComponent.class, - "PhysicsBodyShape", - PhysicsBodyShapeComponent.CODEC)); - setField(plugin, - "physicsBodyDynamicsComponentType", - registry.registerComponent(PhysicsBodyDynamicsComponent.class, - "PhysicsBodyDynamics", - PhysicsBodyDynamicsComponent.CODEC)); - setField(plugin, - "physicsBodyMaterialComponentType", - registry.registerComponent(PhysicsBodyMaterialComponent.class, - "PhysicsBodyMaterial", - PhysicsBodyMaterialComponent.CODEC)); - setField(plugin, - "physicsBodyCollisionComponentType", - registry.registerComponent(PhysicsBodyCollisionComponent.class, - "PhysicsBodyCollision", - PhysicsBodyCollisionComponent.CODEC)); - setField(plugin, - "physicsBodyKinematicTargetComponentType", - registry.registerComponent(PhysicsBodyKinematicTargetComponent.class, - "PhysicsBodyKinematicTarget", - PhysicsBodyKinematicTargetComponent.CODEC)); - staticField(ImpulsePlugin.class, "instance").set(null, plugin); - } - - @Test - void physicsBodyCenterConvertsBackToVisualBasePosition() { - Vector3d visualPosition = ExamplePhysicsOriginMath.visualPositionFromBodyCenter(new Vector3d(1.0, 2.5, 3.0), - PhysicsShapeSpec.box(0.5f, 0.5f, 0.5f)); - - assertEquals(1.0, visualPosition.x, 0.0001); - assertEquals(2.0, visualPosition.y, 0.0001); - assertEquals(3.0, visualPosition.z, 0.0001); - } - - @Test - void ecsAuthoredDynamicBodyHolderAddsControllableMarkerWhenControlIsAvailable() { - Holder holder = registry.newHolder(); - - ExamplePhysicsUtils.addControllableMarkerIfAvailable(holder, PhysicsBodyType.DYNAMIC); - - assertTrue(holder.getArchetype().contains(ImpulseControllableComponent.getComponentType())); - } - - @Nonnull - private static T allocate(@Nonnull Class type) throws Exception { - Class unsafeType = Class.forName("sun.misc.Unsafe"); - Field unsafeField = unsafeType.getDeclaredField("theUnsafe"); - unsafeField.setAccessible(true); - Object unsafe = unsafeField.get(null); - return type.cast(unsafeType.getMethod("allocateInstance", Class.class) - .invoke(unsafe, type)); - } - - private static void setField(@Nonnull Object target, - @Nonnull String name, - @Nonnull Object value) throws Exception { - Field field = staticField(target.getClass(), name); - field.set(target, value); - } - - @Nonnull - private static Field staticField(@Nonnull Class owner, @Nonnull String name) throws Exception { - Field field = owner.getDeclaredField(name); - field.setAccessible(true); - return field; - } -} diff --git a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/events/PhysicsEventTrackerTest.java b/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/events/PhysicsEventTrackerTest.java deleted file mode 100644 index bb0ccd59..00000000 --- a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/events/PhysicsEventTrackerTest.java +++ /dev/null @@ -1,53 +0,0 @@ -package dev.hytalemodding.impulse.examples.events; - -import static org.junit.jupiter.api.Assertions.assertTrue; - -import dev.hytalemodding.impulse.api.PhysicsContactPhase; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.core.plugin.body.RigidBodyKey; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsContactEvent; -import dev.hytalemodding.impulse.core.plugin.events.PhysicsEventFrame; -import java.util.List; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class PhysicsEventTrackerTest { - - @Test - void tracksContactEventsFromPublishedFrames() { - PhysicsEventTracker.reset(); - RigidBodyKey first = RigidBodyKey.of(1L, 2L); - RigidBodyKey second = RigidBodyKey.of(3L, 4L); - PhysicsEventFrame frame = new PhysicsEventFrame(12L, - 34L, - 56L, - 78L, - 90L, - 11L, - List.of(), - List.of(), - List.of(), - List.of(new PhysicsContactEvent(new SpaceId(5), - PhysicsContactPhase.OBSERVED, - first, - second, - new Vector3f(1.0f, 2.0f, 3.0f), - new Vector3f(4.0f, 5.0f, 6.0f), - new Vector3f(0.0f, 1.0f, 0.0f), - -0.125f, - 2.5f)), - 1); - - PhysicsEventTracker.record(frame); - - String snapshot = PhysicsEventTracker.snapshot(); - assertTrue(snapshot.contains("trackedFrames=1")); - assertTrue(snapshot.contains("trackedPhysicsEvents=1")); - assertTrue(snapshot.contains("trackedContacts=1")); - assertTrue(snapshot.contains("firstContact=observed")); - assertTrue(snapshot.contains("space=5")); - assertTrue(snapshot.contains("bodyA=" + first)); - assertTrue(snapshot.contains("bodyB=" + second)); - assertTrue(snapshot.contains("droppedBackendEvents=1")); - } -} diff --git a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveBlockPolicyTest.java b/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveBlockPolicyTest.java deleted file mode 100644 index 4a4c8e87..00000000 --- a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveBlockPolicyTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package dev.hytalemodding.impulse.examples.explosive; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.hypixel.hytale.math.shape.Box; -import com.hypixel.hytale.protocol.BlockMaterial; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class ExplosiveBlockPolicyTest { - - @Test - void outwardImpulsePushesAwayFromExplosionWithVerticalLift() { - Vector3f impulse = ExplosiveBlockPolicy.outwardImpulse( - new Vector3f(0.5f, 10.5f, 0.5f), - new Vector3f(2.5f, 10.5f, 0.5f), - 12.0f, - 0.35f); - - assertTrue(impulse.x > 11.0f); - assertTrue(impulse.y > 3.0f); - assertEquals(0.0f, impulse.z, 0.0001f); - } - - @Test - void centeredBlocksStillReceiveUpwardImpulse() { - Vector3f impulse = ExplosiveBlockPolicy.outwardImpulse( - new Vector3f(4.5f, 8.5f, 4.5f), - new Vector3f(4.5f, 8.5f, 4.5f), - 6.0f, - 0.5f); - - assertEquals(0.0f, impulse.x, 0.0001f); - assertEquals(6.0f, impulse.y, 0.0001f); - assertEquals(0.0f, impulse.z, 0.0001f); - } - - @Test - void ignoresAirAndUnknownBlocksWhenCreatingFragments() { - assertFalse(ExplosiveBlockPolicy.isFragmentCandidate(0)); - assertFalse(ExplosiveBlockPolicy.isFragmentCandidate(1)); - assertTrue(ExplosiveBlockPolicy.isFragmentCandidate(42)); - } - - @Test - void simpleFragmentsRequireSolidFullUnitCollisionBox() { - Box unitBox = new Box(0.0, 0.0, 0.0, 1.0, 1.0, 1.0); - - assertTrue(ExplosiveBlockPolicy.isSimpleFullCubeFragmentBlock(42, - false, - BlockMaterial.Solid, - new Box[] { unitBox })); - assertFalse(ExplosiveBlockPolicy.isSimpleFullCubeFragmentBlock(0, - false, - BlockMaterial.Solid, - new Box[] { unitBox })); - assertFalse(ExplosiveBlockPolicy.isSimpleFullCubeFragmentBlock(42, - true, - BlockMaterial.Solid, - new Box[] { unitBox })); - assertFalse(ExplosiveBlockPolicy.isSimpleFullCubeFragmentBlock(42, - false, - BlockMaterial.Empty, - new Box[] { unitBox })); - assertFalse(ExplosiveBlockPolicy.isSimpleFullCubeFragmentBlock(42, - false, - BlockMaterial.Solid, - new Box[] { new Box(0.0, 0.0, 0.0, 1.0, 0.5, 1.0) })); - assertFalse(ExplosiveBlockPolicy.isSimpleFullCubeFragmentBlock(42, - false, - BlockMaterial.Solid, - new Box[] { unitBox, unitBox })); - } -} diff --git a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveBlockRuntimeTest.java b/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveBlockRuntimeTest.java deleted file mode 100644 index 64f4f84d..00000000 --- a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveBlockRuntimeTest.java +++ /dev/null @@ -1,367 +0,0 @@ -package dev.hytalemodding.impulse.examples.explosive; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.hypixel.hytale.math.vector.Rotation3f; -import com.hypixel.hytale.protocol.EntityPart; -import com.hypixel.hytale.server.core.asset.type.blocktype.config.Rotation; -import com.hypixel.hytale.server.core.asset.type.blocktype.config.RotationTuple; -import com.hypixel.hytale.server.core.asset.type.model.config.ModelParticle; -import com.hypixel.hytale.server.core.entity.ExplosionConfig; -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; -import javax.annotation.Nonnull; -import org.joml.Quaterniond; -import org.joml.Quaternionf; -import org.joml.Vector3d; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class ExplosiveBlockRuntimeTest { - - @Test - void explosionConfigDamagesEntitiesWithoutConsumingTerrain() throws ReflectiveOperationException { - ExplosionConfig config = ExplosiveBlockRuntime.explosionConfig(4); - - assertFalse(booleanField(config, "damageBlocks")); - assertTrue(booleanField(config, "damageEntities")); - assertEquals(0, intField(config, "blockDamageRadius")); - assertEquals(4.0f, floatField(config, "entityDamageRadius"), 0.0001f); - assertEquals(0.0f, floatField(config, "blockDropChance"), 0.0001f); - assertEquals("SFX_Goblin_Lobber_Bomb_Death", objectField(config, "soundEventId")); - - ModelParticle[] particles = (ModelParticle[]) objectField(config, "particles"); - assertEquals(1, particles.length); - assertEquals("Explosion_Medium", particles[0].getSystemId()); - assertEquals(EntityPart.Entity, particles[0].getTargetEntityPart()); - } - - @Test - void chunkBlockCoordinateConvertsWorldCoordinateToLocalCoordinate() { - assertEquals(0, ExplosiveBlockRuntime.chunkBlockCoordinate(32)); - assertEquals(15, ExplosiveBlockRuntime.chunkBlockCoordinate(47)); - assertEquals(31, ExplosiveBlockRuntime.chunkBlockCoordinate(-1)); - } - - @Test - void sourceExplosionCenterBiasesAbovePhysicsBodyCenter() { - Vector3d center = ExplosiveBlockRuntime.sourceExplosionCenter( - new Vector3d(12.5, 40.5, -3.5)); - - assertEquals(new Vector3d(12.5, 41.5, -3.5), center); - } - - @Test - void contactExplosionCenterStartsAboveTerrainContactPoint() { - Vector3d center = ExplosiveBlockRuntime.contactExplosionCenter( - new Vector3d(12.5, 41.0, -3.5)); - - assertEquals(new Vector3d(12.5, 41.5, -3.5), center); - } - - @Test - void fragmentOffsetsAreNearestFirstSphereInsteadOfBottomFirstScan() { - List offsets = - ExplosiveBlockRuntime.sphericalFragmentOffsets(2); - - assertEquals(new ExplosiveBlockRuntime.FragmentOffset(0, 0, 0, 0), offsets.getFirst()); - for (int i = 1; i < offsets.size(); i++) { - assertTrue(offsets.get(i - 1).distanceSquared() <= offsets.get(i).distanceSquared()); - } - - List firstSeven = offsets.subList(0, 7); - assertTrue(firstSeven.stream().anyMatch(offset -> offset.dy() > 0)); - assertTrue(firstSeven.stream().anyMatch(offset -> offset.dy() < 0)); - assertTrue(firstSeven.stream().noneMatch(offset -> offset.dy() < -1)); - } - - @Test - void largeSphereHasEnoughOffsetsForBigExplosionFragmentCap() { - List offsets = - ExplosiveBlockRuntime.sphericalFragmentOffsets(8); - - assertTrue(offsets.size() >= 1024); - } - - @Test - void faceConnectedFragmentsBecomeOneAabbGroup() { - List groups = ExplosiveBlockRuntime.groupFragments( - List.of( - new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", 0, 10, 0), - new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", 1, 10, 0), - new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", 2, 10, 0) - ), - new Vector3d(1.5, 10.5, 0.5), - 8); - - assertEquals(1, groups.size()); - ExplosiveBlockRuntime.FragmentGroup group = groups.getFirst(); - assertEquals("Hytale:block/stone", group.blockType()); - assertEquals(3, group.blockCount()); - assertEquals(new Vector3d(1.5, 10.5, 0.5), group.center()); - assertEquals(1.5f, group.halfExtentX(), 0.0001f); - assertEquals(0.5f, group.halfExtentY(), 0.0001f); - assertEquals(0.5f, group.halfExtentZ(), 0.0001f); - assertEquals(3.0f, group.mass(), 0.0001f); - } - - @Test - void groupedFragmentVisualsUseBlockBasePositionsAndSyncOffsets() { - List groups = ExplosiveBlockRuntime.groupFragments( - List.of( - new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", 0, 10, 0), - new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", 1, 10, 0), - new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", 2, 10, 0) - ), - new Vector3d(1.5, 10.5, 0.5), - 8); - - List visuals = groups.getFirst().visualBlocks(); - - assertEquals(3, visuals.size()); - assertVectorEquals(new Vector3d(1.5, 10.0, 0.5), visuals.getFirst().position()); - assertVectorEquals(new Vector3f(0.0f, 0.0f, 0.0f), visuals.get(0).localPositionOffset()); - assertVisualSyncsToSpawnPosition(groups.getFirst(), visuals.get(0)); - assertVectorEquals(new Vector3d(0.5, 10.0, 0.5), visuals.get(1).position()); - assertVectorEquals(new Vector3f(-1.0f, 0.0f, 0.0f), visuals.get(1).localPositionOffset()); - assertVisualSyncsToSpawnPosition(groups.getFirst(), visuals.get(1)); - assertVectorEquals(new Vector3d(2.5, 10.0, 0.5), visuals.get(2).position()); - assertVectorEquals(new Vector3f(1.0f, 0.0f, 0.0f), visuals.get(2).localPositionOffset()); - assertVisualSyncsToSpawnPosition(groups.getFirst(), visuals.get(2)); - } - - @Test - void groupedFragmentVisualsUseBlockTypeCenterForVisualBase() { - List groups = ExplosiveBlockRuntime.groupFragments( - List.of(new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/offset", - 4, - 10, - -2, - new Vector3d(0.25, 0.5, 0.75))), - new Vector3d(4.5, 10.5, -1.5), - 8); - - List visuals = groups.getFirst().visualBlocks(); - - assertEquals(1, visuals.size()); - assertVectorEquals(new Vector3d(4.25, 10.0, -1.25), visuals.getFirst().position()); - assertVectorEquals(new Vector3f(-0.25f, 0.0f, 0.25f), - visuals.getFirst().localPositionOffset()); - } - - @Test - void blockRotationUsesHytaleRotationTupleIndex() { - RotationTuple rotationTuple = RotationTuple.of(Rotation.Ninety, - Rotation.OneEighty, - Rotation.TwoSeventy); - Rotation3f hytaleRotation = new Rotation3f(); - rotationTuple.applyRotationTo(hytaleRotation); - Quaterniond expected = hytaleRotation.getQuaternion(new Quaterniond()); - - assertQuaternionEquals(expected, ExplosiveBlockRuntime.blockRotation(rotationTuple.index())); - } - - @Test - void groupedFragmentVisualsPreserveBlockLocalRotation() { - Quaternionf localRotation = new Quaternionf().rotateXYZ(0.25f, 0.5f, 0.75f); - List groups = ExplosiveBlockRuntime.groupFragments( - List.of(new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", - 0, - 10, - 0, - new Vector3d(0.5, 0.5, 0.5), - localRotation)), - new Vector3d(0.5, 10.5, 0.5), - 8); - - ExplosiveBlockRuntime.FragmentVisual visual = groups.getFirst().visualBlocks().getFirst(); - - assertQuaternionEquals(localRotation, visual.localRotationOffset()); - visual.localRotationOffset().identity(); - assertQuaternionEquals(localRotation, visual.localRotationOffset()); - } - - @Test - void verticalGroupedFragmentsKeepBlockHeightVisualSpacing() { - List groups = ExplosiveBlockRuntime.groupFragments( - List.of( - new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", 0, 10, 0), - new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", 0, 11, 0) - ), - new Vector3d(0.5, 10.5, 0.5), - 8); - - List visuals = groups.getFirst().visualBlocks(); - - assertEquals(2, visuals.size()); - assertVectorEquals(new Vector3d(0.5, 10.0, 0.5), visuals.getFirst().position()); - assertVectorEquals(new Vector3f(0.0f, -0.5f, 0.0f), visuals.get(0).localPositionOffset()); - assertVisualSyncsToSpawnPosition(groups.getFirst(), visuals.get(0)); - assertVectorEquals(new Vector3d(0.5, 11.0, 0.5), visuals.get(1).position()); - assertVectorEquals(new Vector3f(0.0f, 0.5f, 0.0f), visuals.get(1).localPositionOffset()); - assertVisualSyncsToSpawnPosition(groups.getFirst(), visuals.get(1)); - } - - @Test - void verticalGroupedFragmentVisualCentersRotateAroundGroupCenter() { - ExplosiveBlockRuntime.FragmentGroup group = ExplosiveBlockRuntime.groupFragments( - List.of( - new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", 0, 10, 0), - new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", 0, 11, 0) - ), - new Vector3d(0.5, 10.5, 0.5), - 8).getFirst(); - List visuals = group.visualBlocks(); - Quaternionf rotation = new Quaternionf().rotateZ((float) (Math.PI / 2.0)); - - assertVisualCenterAfterSyncEqualsRotatedLocalCenter(group, - visuals.get(0), - rotation, - new Vector3d(1.0, 11.0, 0.5)); - assertVisualCenterAfterSyncEqualsRotatedLocalCenter(group, - visuals.get(1), - rotation, - new Vector3d(0.0, 11.0, 0.5)); - } - - @Test - void disconnectedFragmentsRemainSeparateGroups() { - List groups = ExplosiveBlockRuntime.groupFragments( - List.of( - new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", 0, 10, 0), - new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/dirt", 4, 10, 0) - ), - new Vector3d(0.5, 10.5, 0.5), - 8); - - assertEquals(2, groups.size()); - assertEquals(new Vector3d(0.5, 10.5, 0.5), groups.get(0).center()); - assertEquals(new Vector3d(4.5, 10.5, 0.5), groups.get(1).center()); - } - - @Test - void largeConnectedComponentsSplitIntoBoundedGroups() { - List fragments = new ArrayList<>(); - for (int x = 0; x < 40; x++) { - fragments.add(new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", x, 10, 0)); - } - - List groups = ExplosiveBlockRuntime.groupFragments( - fragments, - new Vector3d(0.5, 10.5, 0.5), - 8); - - assertTrue(groups.size() > 1); - assertEquals(40, groups.stream().mapToInt(ExplosiveBlockRuntime.FragmentGroup::blockCount).sum()); - assertTrue(groups.stream() - .allMatch(group -> group.blockCount() <= ExplosiveBlockRuntime.MAX_BLOCKS_PER_FRAGMENT_GROUP)); - } - - @Test - void irregularComponentsSplitIntoSolidAabbGroups() { - List groups = ExplosiveBlockRuntime.groupFragments( - List.of( - new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", 0, 10, 0), - new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", 1, 10, 0), - new ExplosiveBlockRuntime.FragmentBlock("Hytale:block/stone", 0, 10, 1) - ), - new Vector3d(0.5, 10.5, 0.5), - 8); - - assertEquals(3, groups.stream().mapToInt(ExplosiveBlockRuntime.FragmentGroup::blockCount).sum()); - assertTrue(groups.size() > 1); - assertTrue(groups.stream() - .allMatch(group -> group.aabbBlockVolume() == group.blockCount())); - } - - @Test - void terrainFragmentsDoNotCarryLandingExplosionState() { - assertNull(ExplosiveBlockRuntime.fragmentLandingExplosionState()); - } - - private static boolean booleanField(ExplosionConfig config, String fieldName) - throws ReflectiveOperationException { - Field field = field(fieldName); - return field.getBoolean(config); - } - - private static int intField(ExplosionConfig config, String fieldName) - throws ReflectiveOperationException { - Field field = field(fieldName); - return field.getInt(config); - } - - private static float floatField(ExplosionConfig config, String fieldName) - throws ReflectiveOperationException { - Field field = field(fieldName); - return field.getFloat(config); - } - - private static Object objectField(ExplosionConfig config, String fieldName) - throws ReflectiveOperationException { - Field field = field(fieldName); - return field.get(config); - } - - private static Field field(String fieldName) throws ReflectiveOperationException { - Field field = ExplosionConfig.class.getDeclaredField(fieldName); - field.setAccessible(true); - return field; - } - - private static void assertVectorEquals(@Nonnull Vector3d expected, @Nonnull Vector3d actual) { - assertEquals(expected.x, actual.x, 0.0001); - assertEquals(expected.y, actual.y, 0.0001); - assertEquals(expected.z, actual.z, 0.0001); - } - - private static void assertVectorEquals(@Nonnull Vector3f expected, @Nonnull Vector3f actual) { - assertEquals(expected.x, actual.x, 0.0001f); - assertEquals(expected.y, actual.y, 0.0001f); - assertEquals(expected.z, actual.z, 0.0001f); - } - - private static void assertQuaternionEquals(@Nonnull Quaterniond expected, - @Nonnull Quaternionf actual) { - assertEquals(expected.x, actual.x, 0.0001f); - assertEquals(expected.y, actual.y, 0.0001f); - assertEquals(expected.z, actual.z, 0.0001f); - assertEquals(expected.w, actual.w, 0.0001f); - } - - private static void assertQuaternionEquals(@Nonnull Quaternionf expected, - @Nonnull Quaternionf actual) { - assertEquals(expected.x, actual.x, 0.0001f); - assertEquals(expected.y, actual.y, 0.0001f); - assertEquals(expected.z, actual.z, 0.0001f); - assertEquals(expected.w, actual.w, 0.0001f); - } - - private static void assertVisualSyncsToSpawnPosition( - ExplosiveBlockRuntime.FragmentGroup group, - ExplosiveBlockRuntime.FragmentVisual visual) { - Vector3d center = group.center(); - Vector3f offset = visual.localPositionOffset(); - assertVectorEquals(visual.position(), new Vector3d( - center.x + offset.x, - center.y - visual.visualOriginOffsetY() + offset.y, - center.z + offset.z)); - } - - private static void assertVisualCenterAfterSyncEqualsRotatedLocalCenter( - ExplosiveBlockRuntime.FragmentGroup group, - ExplosiveBlockRuntime.FragmentVisual visual, - Quaternionf rotation, - Vector3d expectedCenter) { - Vector3f rotatedOffset = rotation.transform(visual.localPositionOffset(), new Vector3f()); - Vector3d syncedBasePosition = new Vector3d(group.center()) - .add(rotatedOffset.x, rotatedOffset.y, rotatedOffset.z) - .sub(0.0, visual.visualOriginOffsetY(), 0.0); - assertVectorEquals(expectedCenter, syncedBasePosition.add(0.0, visual.visualOriginOffsetY(), 0.0)); - } - -} diff --git a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveFuseComponentTest.java b/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveFuseComponentTest.java deleted file mode 100644 index 7a8cf1d8..00000000 --- a/impulse-examples/src/test/java/dev/hytalemodding/impulse/examples/explosive/ExplosiveFuseComponentTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package dev.hytalemodding.impulse.examples.explosive; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import org.joml.Vector3d; -import org.junit.jupiter.api.Test; - -class ExplosiveFuseComponentTest { - - @Test - void armStartsOneSecondFuseOnlyOnce() { - ExplosiveFuseComponent fuse = new ExplosiveFuseComponent(); - - assertFalse(fuse.isDue(119L)); - - assertTrue(fuse.arm(100L)); - assertFalse(fuse.isDue(119L)); - assertTrue(fuse.isDue(120L)); - - assertFalse(fuse.arm(105L)); - assertTrue(fuse.isDue(120L)); - } - - @Test - void verticalVelocityObservationArmsAfterFirstBounce() { - ExplosiveFuseComponent fuse = new ExplosiveFuseComponent(); - - assertFalse(fuse.observeVerticalVelocity(-0.1f, 10L)); - assertTrue(fuse.observeVerticalVelocity(-0.4f, 11L)); - assertFalse(fuse.isDue(31L)); - - Vector3d bounceCenter = new Vector3d(4.5, 8.5, -2.5); - assertTrue(fuse.observeVerticalVelocity(0.15f, 12L, bounceCenter)); - assertFalse(fuse.isDue(31L)); - assertTrue(fuse.isDue(32L)); - assertEquals(bounceCenter, fuse.explosionCenterOr(new Vector3d())); - assertFalse(fuse.observeVerticalVelocity(0.2f, 13L)); - } - - @Test - void verticalVelocityObservationArmsWhenFallingBodySettlesWithoutBounce() { - ExplosiveFuseComponent fuse = new ExplosiveFuseComponent(); - - assertTrue(fuse.observeVerticalVelocity(-0.4f, 11L)); - - Vector3d settledCenter = new Vector3d(4.5, 8.5, -2.5); - assertTrue(fuse.observeVerticalVelocity(0.0f, 12L, settledCenter)); - assertFalse(fuse.isDue(31L)); - assertTrue(fuse.isDue(32L)); - assertEquals(settledCenter, fuse.explosionCenterOr(new Vector3d())); - } -} diff --git a/impulse-native-loader/build.gradle.kts b/impulse-native-loader/build.gradle.kts deleted file mode 100644 index b600092f..00000000 --- a/impulse-native-loader/build.gradle.kts +++ /dev/null @@ -1,3 +0,0 @@ -plugins { - id("java-library") -} diff --git a/impulse-rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierBackend.java b/impulse-rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierBackend.java deleted file mode 100644 index bd018843..00000000 --- a/impulse-rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierBackend.java +++ /dev/null @@ -1,52 +0,0 @@ -package dev.hytalemodding.impulse.rapier; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.PhysicsBackend; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.SpaceId; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import java.util.logging.Logger; -import javax.annotation.Nonnull; - -public final class RapierBackend implements PhysicsBackend { - - public static final BackendId ID = new BackendId("impulse:rapier"); - private static final Logger LOGGER = Logger.getLogger("Impulse"); - private static final AtomicInteger SPACES_CREATED = new AtomicInteger(0); - - private volatile boolean initialized; - - @Nonnull - @Override - public BackendId getId() { - return ID; - } - - @Override - public synchronized void init() { - if (initialized) { - return; - } - - RapierNative.load(); - initialized = true; - LOGGER.log(Level.INFO, "Rapier backend initialized"); - } - - @Nonnull - @Override - public PhysicsSpace createSpace() { - return createSpace(SpaceId.next()); - } - - @Nonnull - @Override - public PhysicsSpace createSpace(@Nonnull SpaceId spaceId) { - int count = SPACES_CREATED.incrementAndGet(); - LOGGER.log(Level.FINE, - "Creating Rapier physics space #" + count + " on thread " - + Thread.currentThread().getName()); - return new RapierSpace(spaceId, this, RapierNative.createSpaceNative()); - } -} diff --git a/impulse-rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierBody.java b/impulse-rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierBody.java deleted file mode 100644 index b525b73a..00000000 --- a/impulse-rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierBody.java +++ /dev/null @@ -1,733 +0,0 @@ -package dev.hytalemodding.impulse.rapier; - -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsBodyType; -import dev.hytalemodding.impulse.api.ShapeType; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -public final class RapierBody implements PhysicsBody { - - private static final float DEFAULT_DYNAMIC_MASS = 1.0f; - - private final ShapeType shapeType; - private final Vector3f boxHalfExtents; - private final float sphereRadius; - private final float halfHeight; - private final PhysicsAxis axis; - private final float centerOfMassOffsetY; - private final Vector3f voxelSize; - private final int[] voxelCoordinates; - - private final Vector3f position = new Vector3f(); - private final Quaternionf rotation = new Quaternionf(); - private final Vector3f linearVelocity = new Vector3f(); - private final Vector3f angularVelocity = new Vector3f(); - private final float[] vec3Scratch = new float[3]; - private final float[] quatScratch = new float[4]; - private final float[] dampingScratch = new float[2]; - private final int[] collisionFilterScratch = new int[2]; - - private float mass; - private float friction = 0.5f; - private float restitution; - private float linearDamping; - private float angularDamping; - private PhysicsBodyType bodyType; - private boolean sensor; - private int collisionGroup = 1; - private int collisionMask = 1; - private boolean continuousCollisionEnabled; - - private RapierSpace space; - private long bodyHandle; - - private RapierBody(@Nonnull ShapeType shapeType, - @Nullable Vector3f boxHalfExtents, - float sphereRadius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float centerOfMassOffsetY, - @Nullable Vector3f voxelSize, - @Nullable int[] voxelCoordinates, - float mass) { - this.shapeType = shapeType; - this.boxHalfExtents = boxHalfExtents != null ? new Vector3f(boxHalfExtents) : null; - this.sphereRadius = sphereRadius; - this.halfHeight = halfHeight; - this.axis = axis; - this.centerOfMassOffsetY = centerOfMassOffsetY; - this.voxelSize = voxelSize != null ? new Vector3f(voxelSize) : null; - this.voxelCoordinates = voxelCoordinates != null ? voxelCoordinates.clone() : null; - this.mass = mass; - this.bodyType = mass <= 0f ? PhysicsBodyType.STATIC : PhysicsBodyType.DYNAMIC; - } - - @Nonnull - static RapierBody box(float halfX, float halfY, float halfZ, float mass) { - return new RapierBody(ShapeType.BOX, new Vector3f(halfX, halfY, halfZ), -1f, - -1f, PhysicsAxis.Y, halfY, null, null, mass); - } - - @Nonnull - static RapierBody sphere(float radius, float mass) { - return new RapierBody(ShapeType.SPHERE, null, radius, -1f, PhysicsAxis.Y, - radius, null, null, mass); - } - - @Nonnull - static RapierBody capsule(float radius, float halfHeight, @Nonnull PhysicsAxis axis, float mass) { - float offsetY = axis == PhysicsAxis.Y ? halfHeight + radius : radius; - return new RapierBody(ShapeType.CAPSULE, null, radius, halfHeight, axis, offsetY, - null, null, mass); - } - - @Nonnull - static RapierBody cylinder(float radius, float halfHeight, @Nonnull PhysicsAxis axis, float mass) { - float offsetY = axis == PhysicsAxis.Y ? halfHeight : radius; - return new RapierBody(ShapeType.CYLINDER, null, radius, halfHeight, axis, offsetY, - null, null, mass); - } - - @Nonnull - static RapierBody cone(float radius, float halfHeight, @Nonnull PhysicsAxis axis, float mass) { - float offsetY = axis == PhysicsAxis.Y ? halfHeight : radius; - return new RapierBody(ShapeType.CONE, null, radius, halfHeight, axis, offsetY, - null, null, mass); - } - - @Nonnull - static RapierBody staticPlane(float groundY) { - RapierBody body = new RapierBody(ShapeType.PLANE, null, -1f, -1f, PhysicsAxis.Y, - 0f, null, null, 0f); - body.position.set(0f, groundY, 0f); - return body; - } - - @Nonnull - static RapierBody voxelTerrain(float voxelSizeX, - float voxelSizeY, - float voxelSizeZ, - @Nonnull int[] voxelCoordinates) { - return new RapierBody(ShapeType.VOXELS, null, -1f, -1f, PhysicsAxis.Y, - 0f, new Vector3f(voxelSizeX, voxelSizeY, voxelSizeZ), voxelCoordinates, 0f); - } - - @Override - public void setPosition(float x, float y, float z) { - position.set(x, y, z); - if (isAttached()) { - RapierNative.setBodyPositionNative(getSpaceHandle(), bodyHandle, x, y, z); - } - } - - @Override - public void setPosition(@Nonnull Vector3f pos) { - setPosition(pos.x, pos.y, pos.z); - } - - @Nonnull - @Override - public Vector3f getPosition() { - Vector3f out = new Vector3f(); - getPosition(out); - return out; - } - - @Override - public void getPosition(@Nonnull Vector3f out) { - if (isAttached()) { - RapierNative.getBodyPositionNative(getSpaceHandle(), bodyHandle, vec3Scratch); - position.set(vec3Scratch[0], vec3Scratch[1], vec3Scratch[2]); - } - out.set(position); - } - - @Override - public void setRotation(float x, float y, float z, float w) { - setStoredRotation(x, y, z, w); - if (isAttached()) { - RapierNative.setBodyRotationNative(getSpaceHandle(), bodyHandle, - rotation.x, rotation.y, rotation.z, rotation.w); - } - } - - @Override - public void setRotation(@Nonnull Quaternionf rot) { - setRotation(rot.x, rot.y, rot.z, rot.w); - } - - @Nonnull - @Override - public Quaternionf getRotation() { - Quaternionf out = new Quaternionf(); - getRotation(out); - return out; - } - - @Override - public void getRotation(@Nonnull Quaternionf out) { - if (isAttached()) { - RapierNative.getBodyRotationNative(getSpaceHandle(), bodyHandle, quatScratch); - setStoredRotation(quatScratch[0], quatScratch[1], quatScratch[2], quatScratch[3]); - } - out.set(rotation); - } - - @Override - public void setRestitution(float restitution) { - this.restitution = restitution; - if (isAttached()) { - RapierNative.setBodyRestitutionNative(getSpaceHandle(), bodyHandle, restitution); - } - } - - @Override - public float getRestitution() { - return restitution; - } - - @Override - public void setFriction(float friction) { - this.friction = friction; - if (isAttached()) { - RapierNative.setBodyFrictionNative(getSpaceHandle(), bodyHandle, friction); - } - } - - @Override - public float getFriction() { - return friction; - } - - @Nonnull - @Override - public PhysicsBodyType getBodyType() { - if (isAttached()) { - bodyType = PhysicsBodyType.values()[RapierNative.getBodyTypeNative(getSpaceHandle(), bodyHandle)]; - } - return bodyType; - } - - @Override - public void setBodyType(@Nonnull PhysicsBodyType bodyType) { - if (bodyType == PhysicsBodyType.STATIC) { - if (mass != 0f) { - setMass(0f); - } - } else if (mass <= 0f) { - setMass(DEFAULT_DYNAMIC_MASS); - } - this.bodyType = bodyType; - if (isAttached()) { - RapierNative.setBodyTypeNative(getSpaceHandle(), bodyHandle, bodyType.ordinal()); - } - } - - @Override - public boolean isStatic() { - return getBodyType() == PhysicsBodyType.STATIC; - } - - @Override - public boolean isKinematic() { - return getBodyType() == PhysicsBodyType.KINEMATIC; - } - - @Override - public void setKinematic(boolean kinematic) { - if (kinematic) { - setBodyType(PhysicsBodyType.KINEMATIC); - } else if (!isStatic()) { - setBodyType(PhysicsBodyType.DYNAMIC); - } - } - - @Override - public void activate() { - if (isAttached()) { - RapierNative.activateBodyNative(getSpaceHandle(), bodyHandle); - } - } - - @Override - public boolean isActive() { - return !isSleeping(); - } - - @Override - public boolean isSleeping() { - return isAttached() && RapierNative.isBodySleepingNative(getSpaceHandle(), bodyHandle); - } - - @Override - public void sleep() { - if (isAttached()) { - RapierNative.sleepBodyNative(getSpaceHandle(), bodyHandle); - } - } - - @Override - public float getMass() { - if (isAttached()) { - mass = RapierNative.getBodyMassNative(getSpaceHandle(), bodyHandle); - } - return mass; - } - - @Override - public void setMass(float mass) { - this.mass = mass; - if (mass <= 0f) { - bodyType = PhysicsBodyType.STATIC; - } - if (isAttached()) { - RapierNative.setBodyMassNative(getSpaceHandle(), bodyHandle, mass); - } - } - - @Nonnull - @Override - public Vector3f getLinearVelocity() { - Vector3f out = new Vector3f(); - getLinearVelocity(out); - return out; - } - - @Override - public void getLinearVelocity(@Nonnull Vector3f out) { - if (isAttached()) { - RapierNative.getBodyLinearVelocityNative(getSpaceHandle(), bodyHandle, vec3Scratch); - linearVelocity.set(vec3Scratch[0], vec3Scratch[1], vec3Scratch[2]); - } - out.set(linearVelocity); - } - - @Override - public void setLinearVelocity(@Nonnull Vector3f vel) { - setLinearVelocity(vel.x, vel.y, vel.z); - } - - @Override - public void setLinearVelocity(float x, float y, float z) { - linearVelocity.set(x, y, z); - if (isAttached()) { - RapierNative.setBodyLinearVelocityNative(getSpaceHandle(), bodyHandle, x, y, z); - } - } - - @Nonnull - @Override - public Vector3f getAngularVelocity() { - Vector3f out = new Vector3f(); - getAngularVelocity(out); - return out; - } - - @Override - public void getAngularVelocity(@Nonnull Vector3f out) { - if (isAttached()) { - RapierNative.getBodyAngularVelocityNative(getSpaceHandle(), bodyHandle, vec3Scratch); - angularVelocity.set(vec3Scratch[0], vec3Scratch[1], vec3Scratch[2]); - } - out.set(angularVelocity); - } - - @Override - public void setAngularVelocity(@Nonnull Vector3f vel) { - setAngularVelocity(vel.x, vel.y, vel.z); - } - - @Override - public void setAngularVelocity(float x, float y, float z) { - angularVelocity.set(x, y, z); - if (isAttached()) { - RapierNative.setBodyAngularVelocityNative(getSpaceHandle(), bodyHandle, x, y, z); - } - } - - @Override - public float getLinearDamping() { - refreshDamping(); - return linearDamping; - } - - @Override - public void setLinearDamping(float damping) { - linearDamping = damping; - pushDamping(); - } - - @Override - public float getAngularDamping() { - refreshDamping(); - return angularDamping; - } - - @Override - public void setAngularDamping(float damping) { - angularDamping = damping; - pushDamping(); - } - - @Override - public void applyCentralForce(@Nonnull Vector3f force) { - applyCentralForce(force.x, force.y, force.z); - } - - @Override - public void applyCentralForce(float x, float y, float z) { - if (isAttached()) { - RapierNative.applyBodyCentralForceNative(getSpaceHandle(), bodyHandle, x, y, z); - } - } - - @Override - public void applyForce(@Nonnull Vector3f force, @Nonnull Vector3f offset) { - applyForce(force.x, force.y, force.z, offset.x, offset.y, offset.z); - } - - @Override - public void applyForce(float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ) { - if (isAttached()) { - RapierNative.applyBodyForceNative(getSpaceHandle(), bodyHandle, - x, y, z, offsetX, offsetY, offsetZ); - } - } - - @Override - public void applyCentralImpulse(@Nonnull Vector3f impulse) { - applyCentralImpulse(impulse.x, impulse.y, impulse.z); - } - - @Override - public void applyCentralImpulse(float x, float y, float z) { - if (isAttached()) { - RapierNative.applyBodyCentralImpulseNative(getSpaceHandle(), bodyHandle, x, y, z); - } - } - - @Override - public void applyImpulse(@Nonnull Vector3f impulse, @Nonnull Vector3f offset) { - applyImpulse(impulse.x, impulse.y, impulse.z, offset.x, offset.y, offset.z); - } - - @Override - public void applyImpulse(float x, - float y, - float z, - float offsetX, - float offsetY, - float offsetZ) { - if (isAttached()) { - RapierNative.applyBodyImpulseNative(getSpaceHandle(), bodyHandle, - x, y, z, offsetX, offsetY, offsetZ); - } - } - - @Override - public void applyTorque(@Nonnull Vector3f torque) { - applyTorque(torque.x, torque.y, torque.z); - } - - @Override - public void applyTorque(float x, float y, float z) { - if (isAttached()) { - RapierNative.applyBodyTorqueNative(getSpaceHandle(), bodyHandle, - x, y, z); - } - } - - @Override - public void applyTorqueImpulse(@Nonnull Vector3f torqueImpulse) { - applyTorqueImpulse(torqueImpulse.x, torqueImpulse.y, torqueImpulse.z); - } - - @Override - public void applyTorqueImpulse(float x, float y, float z) { - if (isAttached()) { - RapierNative.applyBodyTorqueImpulseNative(getSpaceHandle(), bodyHandle, - x, y, z); - } - } - - @Override - public void clearForces() { - if (isAttached()) { - RapierNative.clearBodyForcesNative(getSpaceHandle(), bodyHandle); - } - } - - @Override - public boolean isSensor() { - if (isAttached()) { - sensor = RapierNative.isBodySensorNative(getSpaceHandle(), bodyHandle); - } - return sensor; - } - - @Override - public void setSensor(boolean sensor) { - this.sensor = sensor; - if (isAttached()) { - RapierNative.setBodySensorNative(getSpaceHandle(), bodyHandle, sensor); - } - } - - @Override - public int getCollisionGroup() { - refreshCollisionFilter(); - return collisionGroup; - } - - @Override - public int getCollisionMask() { - refreshCollisionFilter(); - return collisionMask; - } - - @Override - public void setCollisionFilter(int group, int mask) { - collisionGroup = group; - collisionMask = mask; - if (isAttached()) { - RapierNative.setBodyCollisionFilterNative(getSpaceHandle(), bodyHandle, group, mask); - } - } - - @Override - public boolean isContinuousCollisionEnabled() { - if (isAttached()) { - continuousCollisionEnabled = RapierNative.isBodyCcdNative(getSpaceHandle(), bodyHandle); - } - return continuousCollisionEnabled; - } - - @Override - public void setContinuousCollisionEnabled(boolean enabled) { - continuousCollisionEnabled = enabled; - if (isAttached()) { - RapierNative.setBodyCcdNative(getSpaceHandle(), bodyHandle, enabled); - } - } - - @Nonnull - @Override - public ShapeType getShapeType() { - return shapeType; - } - - @Nullable - @Override - public Vector3f getBoxHalfExtents() { - return boxHalfExtents != null ? new Vector3f(boxHalfExtents) : null; - } - - @Override - public float getSphereRadius() { - return sphereRadius; - } - - @Override - public float getHalfHeight() { - return halfHeight; - } - - @Nonnull - @Override - public PhysicsAxis getShapeAxis() { - return axis; - } - - @Override - public float getCenterOfMassOffsetY() { - return centerOfMassOffsetY; - } - - boolean isAttached() { - return space != null && bodyHandle != 0L; - } - - boolean isAttachedTo(@Nonnull RapierSpace owner) { - return space == owner && bodyHandle != 0L; - } - - void attach(@Nonnull RapierSpace space, long bodyHandle) { - if (isAttached()) { - throw new IllegalStateException("Rapier body is already attached to a space"); - } - this.space = space; - this.bodyHandle = bodyHandle; - } - - void detach(@Nonnull RapierSpace owner) { - if (space != owner) { - return; - } - space = null; - bodyHandle = 0L; - } - - long getBodyHandle() { - return bodyHandle; - } - - float getStoredMass() { - return mass; - } - - float getStoredFriction() { - return friction; - } - - float getStoredRestitution() { - return restitution; - } - - float getStoredLinearDamping() { - return linearDamping; - } - - float getStoredAngularDamping() { - return angularDamping; - } - - boolean getStoredSensor() { - return sensor; - } - - int getStoredCollisionGroup() { - return collisionGroup; - } - - int getStoredCollisionMask() { - return collisionMask; - } - - boolean getStoredContinuousCollisionEnabled() { - return continuousCollisionEnabled; - } - - @Nonnull - PhysicsBodyType getStoredBodyType() { - return bodyType; - } - - @Nonnull - PhysicsBodySnapshot snapshotFromNative(@Nonnull float[] values, - int offset, - @Nullable PhysicsBodySnapshot previous) { - boolean nativeSleeping = values[offset + 14] != 0.0f; - if (nativeSleeping && previous != null && previous.sleeping()) { - return previous; - } - - position.set(values[offset], values[offset + 1], values[offset + 2]); - setStoredRotation(values[offset + 3], values[offset + 4], values[offset + 5], - values[offset + 6]); - linearVelocity.set(values[offset + 7], values[offset + 8], values[offset + 9]); - angularVelocity.set(values[offset + 10], values[offset + 11], values[offset + 12]); - - int bodyTypeOrdinal = Math.round(values[offset + 13]); - PhysicsBodyType[] bodyTypes = PhysicsBodyType.values(); - if (bodyTypeOrdinal >= 0 && bodyTypeOrdinal < bodyTypes.length) { - bodyType = bodyTypes[bodyTypeOrdinal]; - } - sensor = values[offset + 15] != 0.0f; - - return new PhysicsBodySnapshot(position, - rotation, - linearVelocity, - angularVelocity, - bodyType, - nativeSleeping, - sensor, - centerOfMassOffsetY, - shapeType, - boxHalfExtents, - sphereRadius, - halfHeight, - axis); - } - - @Nonnull - Vector3f getStoredPosition() { - return new Vector3f(position); - } - - @Nonnull - Quaternionf getStoredRotation() { - return new Quaternionf(rotation); - } - - @Nonnull - Vector3f getStoredLinearVelocity() { - return new Vector3f(linearVelocity); - } - - @Nonnull - Vector3f getStoredAngularVelocity() { - return new Vector3f(angularVelocity); - } - - @Nonnull - Vector3f getVoxelSize() { - if (voxelSize == null) { - throw new IllegalStateException("Body is not a voxel terrain body"); - } - return new Vector3f(voxelSize); - } - - @Nonnull - int[] getVoxelCoordinates() { - if (voxelCoordinates == null) { - throw new IllegalStateException("Body is not a voxel terrain body"); - } - return voxelCoordinates.clone(); - } - - private long getSpaceHandle() { - return space.getNativeSpaceHandle(); - } - - private void refreshDamping() { - if (isAttached()) { - RapierNative.getBodyDampingNative(getSpaceHandle(), bodyHandle, dampingScratch); - linearDamping = dampingScratch[0]; - angularDamping = dampingScratch[1]; - } - } - - private void pushDamping() { - if (isAttached()) { - RapierNative.setBodyDampingNative(getSpaceHandle(), bodyHandle, - linearDamping, angularDamping); - } - } - - private void refreshCollisionFilter() { - if (isAttached()) { - RapierNative.getBodyCollisionFilterNative(getSpaceHandle(), bodyHandle, collisionFilterScratch); - collisionGroup = collisionFilterScratch[0]; - collisionMask = collisionFilterScratch[1]; - } - } - - private void setStoredRotation(float x, float y, float z, float w) { - rotation.set(x, y, z, w); - if (rotation.lengthSquared() == 0f) { - rotation.identity(); - return; - } - rotation.normalize(); - } -} diff --git a/impulse-rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierJoint.java b/impulse-rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierJoint.java deleted file mode 100644 index 85d27d9b..00000000 --- a/impulse-rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierJoint.java +++ /dev/null @@ -1,217 +0,0 @@ -package dev.hytalemodding.impulse.rapier; - -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsJoint; -import dev.hytalemodding.impulse.api.PhysicsJointType; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import org.joml.Vector3f; - -final class RapierJoint implements PhysicsJoint { - - private final RapierSpace space; - private final PhysicsJointType type; - private final RapierBody bodyA; - private final RapierBody bodyB; - private final long jointHandle; - private final float anchorAX; - private final float anchorAY; - private final float anchorAZ; - private final float anchorBX; - private final float anchorBY; - private final float anchorBZ; - private final float axisX; - private final float axisY; - private final float axisZ; - private float lowerLimit; - private float upperLimit; - private boolean motorEnabled; - private float motorTargetVelocity; - private float motorMaxForce; - private final float springRestLength; - private final float springStiffness; - private final float springDamping; - private boolean valid = true; - - RapierJoint(@Nonnull RapierSpace space, - @Nonnull PhysicsJointType type, - @Nonnull RapierBody bodyA, - @Nonnull RapierBody bodyB, - long jointHandle, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ, - float springRestLength, - float springStiffness, - float springDamping) { - this.space = space; - this.type = type; - this.bodyA = bodyA; - this.bodyB = bodyB; - this.jointHandle = jointHandle; - this.anchorAX = anchorAX; - this.anchorAY = anchorAY; - this.anchorAZ = anchorAZ; - this.anchorBX = anchorBX; - this.anchorBY = anchorBY; - this.anchorBZ = anchorBZ; - this.axisX = axisX; - this.axisY = axisY; - this.axisZ = axisZ; - this.springRestLength = springRestLength; - this.springStiffness = springStiffness; - this.springDamping = springDamping; - } - - @Nonnull - @Override - public PhysicsJointType getType() { - return type; - } - - @Nonnull - @Override - public PhysicsBody getBodyA() { - return bodyA; - } - - @Nonnull - @Override - public PhysicsBody getBodyB() { - return bodyB; - } - - @Override - public boolean isEnabled() { - if (!canUseNative()) { - return false; - } - return RapierNative.isJointEnabledNative(space.getNativeSpaceHandle(), jointHandle); - } - - @Override - public void setEnabled(boolean enabled) { - if (!canUseNative()) { - return; - } - RapierNative.setJointEnabledNative(space.getNativeSpaceHandle(), jointHandle, enabled); - } - - @Nonnull - @Override - public Vector3f getAnchorA() { - return new Vector3f(anchorAX, anchorAY, anchorAZ); - } - - @Nonnull - @Override - public Vector3f getAnchorB() { - return new Vector3f(anchorBX, anchorBY, anchorBZ); - } - - @Nullable - @Override - public Vector3f getAxis() { - return new Vector3f(axisX, axisY, axisZ); - } - - @Override - public float getLowerLimit() { - return lowerLimit; - } - - @Override - public float getUpperLimit() { - return upperLimit; - } - - @Override - public void setLimits(float lowerLimit, float upperLimit) { - this.lowerLimit = lowerLimit; - this.upperLimit = upperLimit; - if (!canUseNative()) { - return; - } - RapierNative.setJointLimitsNative(space.getNativeSpaceHandle(), jointHandle, - lowerLimit, upperLimit); - } - - @Override - public boolean isMotorEnabled() { - return motorEnabled; - } - - @Override - public void setMotorEnabled(boolean enabled) { - motorEnabled = enabled; - pushMotor(); - } - - @Override - public float getMotorTargetVelocity() { - return motorTargetVelocity; - } - - @Override - public float getMotorMaxForce() { - return motorMaxForce; - } - - @Override - public void setMotor(float targetVelocity, float maxForce) { - motorTargetVelocity = targetVelocity; - motorMaxForce = maxForce; - pushMotor(); - } - - @Override - public float getSpringRestLength() { - return springRestLength; - } - - @Override - public float getSpringStiffness() { - return springStiffness; - } - - @Override - public float getSpringDamping() { - return springDamping; - } - - long getJointHandle() { - return jointHandle; - } - - boolean belongsTo(@Nonnull RapierSpace owner) { - return space == owner; - } - - boolean isValidIn(@Nonnull RapierSpace owner) { - return belongsTo(owner) && valid && !space.isClosed(); - } - - void invalidate(@Nonnull RapierSpace owner) { - if (space == owner) { - valid = false; - } - } - - private void pushMotor() { - if (!canUseNative()) { - return; - } - RapierNative.setJointMotorNative(space.getNativeSpaceHandle(), jointHandle, - motorEnabled, motorTargetVelocity, motorMaxForce); - } - - private boolean canUseNative() { - return valid && !space.isClosed(); - } -} diff --git a/impulse-rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierSpace.java b/impulse-rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierSpace.java deleted file mode 100644 index 7002d843..00000000 --- a/impulse-rapier/src/main/java/dev/hytalemodding/impulse/rapier/RapierSpace.java +++ /dev/null @@ -1,1205 +0,0 @@ -package dev.hytalemodding.impulse.rapier; - -import dev.hytalemodding.impulse.api.BackendId; -import dev.hytalemodding.impulse.api.PhysicsAxis; -import dev.hytalemodding.impulse.api.PhysicsBackendEventKind; -import dev.hytalemodding.impulse.api.PhysicsBackendEventSink; -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsBodySnapshot; -import dev.hytalemodding.impulse.api.PhysicsContact; -import dev.hytalemodding.impulse.api.PhysicsContactPhase; -import dev.hytalemodding.impulse.api.PhysicsJoint; -import dev.hytalemodding.impulse.api.PhysicsJointType; -import dev.hytalemodding.impulse.api.PhysicsRayHit; -import dev.hytalemodding.impulse.api.PhysicsRuntimeStats; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.PhysicsStepPhaseStats; -import dev.hytalemodding.impulse.api.ShapeType; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.capability.PhysicsActivationTuning; -import dev.hytalemodding.impulse.api.capability.PhysicsActivationTuningCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsBackendEventsCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsCapabilityDescriptor; -import dev.hytalemodding.impulse.api.capability.PhysicsCapabilityId; -import dev.hytalemodding.impulse.api.capability.PhysicsContinuousCollisionCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsExtensionSettingsCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsSolverTuning; -import dev.hytalemodding.impulse.api.capability.PhysicsSolverTuningCapability; -import dev.hytalemodding.impulse.api.capability.PhysicsVoxelTerrainCapability; -import java.lang.ref.Cleaner; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; -import javax.annotation.Nonnull; -import org.joml.Quaternionf; -import org.joml.Vector3f; - -public final class RapierSpace implements PhysicsSpace { - - private static final Cleaner CLEANER = Cleaner.create(); - private static final PhysicsCapabilityId RAPIER_SOLVER_EXTENSION_ID = - new PhysicsCapabilityId("impulse:rapier_solver"); - private static final String INTERNAL_PGS_ITERATIONS = "internalPgsIterations"; - private static final String MIN_ISLAND_SIZE = "minIslandSize"; - private static final PhysicsCapabilityDescriptor RAPIER_SOLVER_EXTENSION_DESCRIPTOR = - new PhysicsCapabilityDescriptor(RAPIER_SOLVER_EXTENSION_ID, - "Rapier solver", - "Configures Rapier-specific solver batching settings"); - private static final List CAPABILITY_DESCRIPTORS = List.of( - PhysicsSolverTuningCapability.DESCRIPTOR, - PhysicsActivationTuningCapability.DESCRIPTOR, - PhysicsContinuousCollisionCapability.DESCRIPTOR, - PhysicsBackendEventsCapability.DESCRIPTOR, - PhysicsVoxelTerrainCapability.DESCRIPTOR, - PhysicsExtensionSettingsCapability.DESCRIPTOR, - RAPIER_SOLVER_EXTENSION_DESCRIPTOR); - private static final int DEFAULT_SOLVER_ITERATIONS = 4; - private static final int DEFAULT_INTERNAL_PGS_ITERATIONS = 1; - private static final int DEFAULT_STABILIZATION_ITERATIONS = 1; - private static final int DEFAULT_MIN_ISLAND_SIZE = 128; - private static final int RAY_HIT_FLOATS = 10; - private static final int CONTACT_FLOATS = 15; - private static final int CONTACT_EVENT_FLOATS = 16; - private static final int BODY_SNAPSHOT_FLOATS = 16; - private static final int RUNTIME_STATS_VALUES = 10; - private static final int STEP_PHASE_STATS_VALUES = 6; - - private final SpaceId id; - private final RapierBackend backend; - private long nativeSpaceHandle; - private final Cleaner.Cleanable cleanable; - private final PhysicsSolverTuningCapability solverTuningCapability = - new RapierSolverTuningCapability(); - private final PhysicsActivationTuningCapability activationTuningCapability = - new RapierActivationTuningCapability(); - private final PhysicsContinuousCollisionCapability continuousCollisionCapability = - new PhysicsContinuousCollisionCapability() { - }; - private final PhysicsBackendEventsCapability backendEventsCapability = - () -> Set.of(PhysicsBackendEventKind.CONTACT_STARTED, - PhysicsBackendEventKind.CONTACT_ENDED, - PhysicsBackendEventKind.CONTACT_FORCE); - private final PhysicsVoxelTerrainCapability voxelTerrainCapability = - new RapierVoxelTerrainCapability(); - private final PhysicsExtensionSettingsCapability extensionSettingsCapability = - new RapierExtensionSettingsCapability(); - private final List bodies = new ArrayList<>(); - private final Map bodiesByHandle = new HashMap<>(); - private final List joints = new ArrayList<>(); - private long[] snapshotBodyHandles = new long[0]; - private float[] snapshotBodyData = new float[0]; - private RapierBody[] selectedSnapshotBodies = new RapierBody[0]; - private int solverIterations = DEFAULT_SOLVER_ITERATIONS; - private int internalPgsIterations = DEFAULT_INTERNAL_PGS_ITERATIONS; - private int stabilizationIterations = DEFAULT_STABILIZATION_ITERATIONS; - private int minIslandSize = DEFAULT_MIN_ISLAND_SIZE; - private boolean closed; - - RapierSpace(@Nonnull SpaceId id, @Nonnull RapierBackend backend, long nativeSpaceHandle) { - if (nativeSpaceHandle == 0L) { - throw new IllegalStateException("Rapier returned a null native space handle"); - } - this.id = id; - this.backend = backend; - this.nativeSpaceHandle = nativeSpaceHandle; - this.cleanable = CLEANER.register(this, new NativeSpaceCleanup(nativeSpaceHandle)); - } - - @Nonnull - @Override - public SpaceId id() { - return id; - } - - @Nonnull - @Override - public BackendId backendId() { - return backend.getId(); - } - - @Override - public void step(float dt) { - if (dt <= 0f) { - return; - } - ensureOpen(); - if (!RapierNative.stepNative(nativeSpaceHandle, dt)) { - throw new IllegalStateException("Rapier native step failed"); - } - } - - @Override - public void step(float dt, @Nonnull PhysicsBackendEventSink events) { - if (dt <= 0f) { - return; - } - ensureOpen(); - float[] raw = RapierNative.stepContactEventsNative(nativeSpaceHandle, dt); - if (raw == null) { - throw new IllegalStateException("Rapier native contact event step failed"); - } - for (int i = 0; i + CONTACT_EVENT_FLOATS <= raw.length; i += CONTACT_EVENT_FLOATS) { - PhysicsContactPhase phase = contactPhase(raw[i]); - if (phase == null) { - continue; - } - RapierBody bodyA = bodiesByHandle.get(rawBitFloatPairToLong(raw[i + 1], raw[i + 2])); - RapierBody bodyB = bodiesByHandle.get(rawBitFloatPairToLong(raw[i + 3], raw[i + 4])); - if (bodyA == null || bodyB == null) { - continue; - } - events.contact(phase, - bodyA, - bodyB, - new Vector3f(raw[i + 5], raw[i + 6], raw[i + 7]), - new Vector3f(raw[i + 8], raw[i + 9], raw[i + 10]), - new Vector3f(raw[i + 11], raw[i + 12], raw[i + 13]), - raw[i + 14], - raw[i + 15]); - } - } - - private static PhysicsContactPhase contactPhase(float rawPhase) { - return switch ((int) rawPhase) { - case 0 -> PhysicsContactPhase.STARTED; - case 2 -> PhysicsContactPhase.ENDED; - case 4 -> PhysicsContactPhase.FORCE; - default -> null; - }; - } - - private void setSolverTuning(int solverIterations, - int internalPgsIterations, - int stabilizationIterations, - int minIslandSize) { - ensureOpen(); - this.solverIterations = solverIterations; - this.internalPgsIterations = internalPgsIterations; - this.stabilizationIterations = stabilizationIterations; - this.minIslandSize = minIslandSize; - RapierNative.setSolverTuningNative(nativeSpaceHandle, - solverIterations, - internalPgsIterations, - stabilizationIterations, - minIslandSize); - } - - private void setDynamicSleepTuning(float linearThreshold, - float angularThreshold, - float timeUntilSleep) { - ensureOpen(); - RapierNative.setDynamicSleepTuningNative(nativeSpaceHandle, - linearThreshold, - angularThreshold, - timeUntilSleep); - } - - private void applyCurrentSolverTuning() { - setSolverTuning(solverIterations, - internalPgsIterations, - stabilizationIterations, - minIslandSize); - } - - @Override - public void setGravity(float x, float y, float z) { - ensureOpen(); - RapierNative.setGravityNative(nativeSpaceHandle, x, y, z); - } - - @Nonnull - @Override - public Vector3f getGravity() { - ensureOpen(); - float[] out = new float[3]; - RapierNative.getGravityNative(nativeSpaceHandle, out); - return new Vector3f(out[0], out[1], out[2]); - } - - @Override - public void addBody(@Nonnull PhysicsBody body) { - ensureOpen(); - if (!(body instanceof RapierBody rapierBody)) { - throw new IllegalArgumentException("Body does not belong to rapier backend"); - } - if (rapierBody.isAttached()) { - throw new IllegalStateException("Rapier body is already attached to a space"); - } - - long nativeBodyHandle = addNativeBody(rapierBody); - if (nativeBodyHandle == 0L) { - throw new IllegalStateException("Rapier returned a null native body handle"); - } - - rapierBody.attach(this, nativeBodyHandle); - bodies.add(rapierBody); - bodiesByHandle.put(nativeBodyHandle, rapierBody); - } - - @Override - public void removeBody(@Nonnull PhysicsBody body) { - if (closed) { - return; - } - if (!(body instanceof RapierBody rapierBody)) { - return; - } - if (!rapierBody.isAttachedTo(this)) { - if (rapierBody.isAttached()) { - throw new IllegalArgumentException("Rapier body belongs to another space"); - } - return; - } - - removeAttachedJoints(rapierBody); - long handle = rapierBody.getBodyHandle(); - RapierNative.removeBodyNative(nativeSpaceHandle, handle); - rapierBody.detach(this); - bodies.remove(rapierBody); - bodiesByHandle.remove(handle); - } - - @Nonnull - @Override - public List getBodies() { - return new ArrayList<>(bodies); - } - - @Override - public int bodyCount() { - return bodies.size(); - } - - @Override - public void forEachBody(@Nonnull Consumer consumer) { - for (RapierBody body : bodies) { - consumer.accept(body); - } - } - - @Override - public boolean containsBody(@Nonnull PhysicsBody body) { - return body instanceof RapierBody rapierBody && rapierBody.isAttachedTo(this); - } - - @Nonnull - @Override - public Optional getCapability(@Nonnull Class type) { - Objects.requireNonNull(type, "type"); - if (type == PhysicsSolverTuningCapability.class) { - return Optional.of(type.cast(solverTuningCapability)); - } - if (type == PhysicsActivationTuningCapability.class) { - return Optional.of(type.cast(activationTuningCapability)); - } - if (type == PhysicsContinuousCollisionCapability.class) { - return Optional.of(type.cast(continuousCollisionCapability)); - } - if (type == PhysicsBackendEventsCapability.class) { - return Optional.of(type.cast(backendEventsCapability)); - } - if (type == PhysicsVoxelTerrainCapability.class) { - return Optional.of(type.cast(voxelTerrainCapability)); - } - if (type == PhysicsExtensionSettingsCapability.class) { - return Optional.of(type.cast(extensionSettingsCapability)); - } - return Optional.empty(); - } - - @Nonnull - @Override - public List getCapabilityDescriptors() { - return CAPABILITY_DESCRIPTORS; - } - - @Override - public void snapshotBodies(@Nonnull Consumer consumer) { - snapshotBodies(_ -> null, consumer); - } - - @Override - public void snapshotBodies( - @Nonnull Function previousSnapshots, - @Nonnull Consumer consumer) { - snapshotBodies(previousSnapshots, (_, snapshot) -> consumer.accept(snapshot)); - } - - @Override - public void snapshotBodies( - @Nonnull Function previousSnapshots, - @Nonnull BiConsumer consumer) { - ensureOpen(); - int count = bodies.size(); - if (count == 0) { - return; - } - - ensureSnapshotCapacity(count); - for (int i = 0; i < count; i++) { - snapshotBodyHandles[i] = bodies.get(i).getBodyHandle(); - } - - int written = RapierNative.snapshotBodiesNative(nativeSpaceHandle, - snapshotBodyHandles, - count, - snapshotBodyData); - if (written < 0) { - throw new IllegalStateException("Rapier native snapshot failed"); - } - int limit = Math.clamp(written, 0, count); - for (int i = 0; i < limit; i++) { - RapierBody body = bodies.get(i); - consumer.accept(body, body.snapshotFromNative(snapshotBodyData, - i * BODY_SNAPSHOT_FLOATS, - previousSnapshots.apply(body))); - } - for (int i = limit; i < count; i++) { - RapierBody body = bodies.get(i); - consumer.accept(body, PhysicsBodySnapshot.from(body, previousSnapshots.apply(body))); - } - } - - @Override - public void snapshotBodies(@Nonnull Iterable selectedBodies, - @Nonnull Function previousSnapshots, - @Nonnull Consumer consumer) { - snapshotBodies(selectedBodies, previousSnapshots, - (_, snapshot) -> consumer.accept(snapshot)); - } - - @Override - public void snapshotBodies(@Nonnull Iterable selectedBodies, - @Nonnull Function previousSnapshots, - @Nonnull BiConsumer consumer) { - ensureOpen(); - int count = collectSelectedSnapshotBodies(selectedBodies, previousSnapshots, consumer); - if (count == 0) { - return; - } - - try { - int written = RapierNative.snapshotBodiesNative(nativeSpaceHandle, - snapshotBodyHandles, - count, - snapshotBodyData); - if (written < 0) { - throw new IllegalStateException("Rapier native snapshot failed"); - } - int limit = Math.clamp(written, 0, count); - for (int i = 0; i < limit; i++) { - RapierBody body = selectedSnapshotBodies[i]; - consumer.accept(body, body.snapshotFromNative(snapshotBodyData, - i * BODY_SNAPSHOT_FLOATS, - previousSnapshots.apply(body))); - } - for (int i = limit; i < count; i++) { - RapierBody body = selectedSnapshotBodies[i]; - consumer.accept(body, - PhysicsBodySnapshot.from(body, previousSnapshots.apply(body))); - } - } finally { - for (int i = 0; i < count; i++) { - selectedSnapshotBodies[i] = null; - } - } - } - - private int collectSelectedSnapshotBodies( - @Nonnull Iterable selectedBodies, - @Nonnull Function previousSnapshots, - @Nonnull BiConsumer consumer) { - int count = 0; - for (PhysicsBody body : selectedBodies) { - if (!(body instanceof RapierBody rapierBody) || !rapierBody.isAttachedTo(this)) { - consumer.accept(body, - PhysicsBodySnapshot.from(body, previousSnapshots.apply(body))); - continue; - } - - ensureSnapshotCapacity(count + 1); - selectedSnapshotBodies[count] = rapierBody; - snapshotBodyHandles[count] = rapierBody.getBodyHandle(); - count++; - } - return count; - } - - private void ensureSnapshotCapacity(int count) { - if (snapshotBodyHandles.length < count) { - int capacity = Math.max(count, snapshotBodyHandles.length * 2); - snapshotBodyHandles = Arrays.copyOf(snapshotBodyHandles, capacity); - } - int floats = count * BODY_SNAPSHOT_FLOATS; - if (snapshotBodyData.length < floats) { - int capacity = Math.max(floats, snapshotBodyData.length * 2); - snapshotBodyData = new float[capacity]; - } - if (selectedSnapshotBodies.length < count) { - int capacity = Math.max(count, selectedSnapshotBodies.length * 2); - selectedSnapshotBodies = Arrays.copyOf(selectedSnapshotBodies, capacity); - } - } - - @Nonnull - @Override - public PhysicsRuntimeStats getRuntimeStats() { - ensureOpen(); - int[] values = RapierNative.getRuntimeStatsNative(nativeSpaceHandle); - if (values == null || values.length < RUNTIME_STATS_VALUES) { - return PhysicsRuntimeStats.unavailable(); - } - return PhysicsRuntimeStats.available(values[0], - values[1], - values[2], - values[3], - values[4], - values[5], - values[6], - values[7], - values[8], - values[9]); - } - - @Override - public void resetStepPhaseStats() { - ensureOpen(); - RapierNative.resetStepPhaseStatsNative(nativeSpaceHandle); - } - - @Nonnull - @Override - public PhysicsStepPhaseStats getStepPhaseStats() { - ensureOpen(); - long[] values = RapierNative.getStepPhaseStatsNative(nativeSpaceHandle); - if (values == null || values.length < STEP_PHASE_STATS_VALUES) { - return PhysicsStepPhaseStats.unavailable(); - } - return PhysicsStepPhaseStats.available(values[0], - values[1], - values[2], - values[3], - values[4], - values[5]); - } - - @Nonnull - @Override - public PhysicsBody createStaticPlane(float groundY) { - return RapierBody.staticPlane(groundY); - } - - @Nonnull - @Override - public PhysicsBody createBox(float halfX, float halfY, float halfZ, float mass) { - return RapierBody.box(halfX, halfY, halfZ, mass); - } - - @Nonnull - @Override - public PhysicsBody createBox(@Nonnull Vector3f halfExtents, float mass) { - return createBox(halfExtents.x, halfExtents.y, halfExtents.z, mass); - } - - @Nonnull - private PhysicsBody createVoxelTerrain(float voxelSizeX, - float voxelSizeY, - float voxelSizeZ, - @Nonnull int[] voxelCoordinates) { - return RapierBody.voxelTerrain(voxelSizeX, voxelSizeY, voxelSizeZ, voxelCoordinates); - } - - private void combineVoxelTerrains(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - int shiftX, - int shiftY, - int shiftZ) { - ensureOpen(); - RapierBody rapierBodyA = requireAttachedBody(bodyA); - RapierBody rapierBodyB = requireAttachedBody(bodyB); - if (rapierBodyA == rapierBodyB) { - throw new IllegalArgumentException("Cannot combine a voxel terrain body with itself"); - } - requireVoxelTerrain(rapierBodyA); - requireVoxelTerrain(rapierBodyB); - if (!RapierNative.combineVoxelTerrainNative(nativeSpaceHandle, - rapierBodyA.getBodyHandle(), - rapierBodyB.getBodyHandle(), - shiftX, - shiftY, - shiftZ)) { - throw new IllegalStateException("Rapier native voxel terrain combine failed"); - } - } - - private static void requireVoxelTerrain(@Nonnull RapierBody body) { - if (body.getShapeType() != ShapeType.VOXELS) { - throw new IllegalArgumentException("Body must be a voxel terrain"); - } - } - - @Nonnull - @Override - public PhysicsBody createSphere(float radius, float mass) { - return RapierBody.sphere(radius, mass); - } - - @Nonnull - @Override - public PhysicsBody createCapsule(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - return RapierBody.capsule(radius, halfHeight, axis, mass); - } - - @Nonnull - @Override - public PhysicsBody createCylinder(float radius, - float halfHeight, - @Nonnull PhysicsAxis axis, - float mass) { - return RapierBody.cylinder(radius, halfHeight, axis, mass); - } - - @Nonnull - @Override - public PhysicsBody createCone(float radius, float halfHeight, @Nonnull PhysicsAxis axis, - float mass) { - return RapierBody.cone(radius, halfHeight, axis, mass); - } - - @Nonnull - @Override - public Optional raycastClosest(@Nonnull Vector3f from, @Nonnull Vector3f to) { - ensureOpen(); - List hits = raycastAll(from, to); - PhysicsRayHit closest = null; - for (PhysicsRayHit hit : hits) { - if (closest == null || hit.fraction() < closest.fraction()) { - closest = hit; - } - } - return Optional.ofNullable(closest); - } - - public long rawBitFloatPairToLong(float upper, float lower) { - long upperBits = Float.floatToRawIntBits(upper); - long lowerBits = Float.floatToRawIntBits(lower) & 0xFFFFFFFFL; - - return (upperBits << 32) | lowerBits; - } - - @Nonnull - @Override - public List raycastAll(@Nonnull Vector3f from, @Nonnull Vector3f to) { - ensureOpen(); - float[] raw = RapierNative.raycastAllNative(nativeSpaceHandle, - from.x, from.y, from.z, to.x, to.y, to.z); - List hits = new ArrayList<>(raw.length / RAY_HIT_FLOATS); - for (int i = 0; i + RAY_HIT_FLOATS <= raw.length; i += RAY_HIT_FLOATS) { - RapierBody body = bodiesByHandle.get(rawBitFloatPairToLong(raw[i], raw[i + 1])); - if (body == null) { - continue; - } - Vector3f point = new Vector3f(raw[i + 2], raw[i + 3], raw[i + 4]); - Vector3f normal = new Vector3f(raw[i + 5], raw[i + 6], raw[i + 7]); - hits.add(new PhysicsRayHit(body, point, normal, raw[i + 8], raw[i + 9])); - } - return hits; - } - - @Nonnull - @Override - public List getContacts() { - ensureOpen(); - float[] raw = RapierNative.getContactsNative(nativeSpaceHandle); - List contacts = new ArrayList<>(raw.length / CONTACT_FLOATS); - for (int i = 0; i + CONTACT_FLOATS <= raw.length; i += CONTACT_FLOATS) { - RapierBody bodyA = bodiesByHandle.get(rawBitFloatPairToLong(raw[i], raw[i + 1])); - RapierBody bodyB = bodiesByHandle.get(rawBitFloatPairToLong(raw[i + 2], raw[i + 3])); - if (bodyA == null || bodyB == null) { - continue; - } - Vector3f pointA = new Vector3f(raw[i + 4], raw[i + 5], raw[i + 6]); - Vector3f pointB = new Vector3f(raw[i + 7], raw[i + 8], raw[i + 9]); - Vector3f normal = new Vector3f(raw[i + 10], raw[i + 11], raw[i + 12]); - contacts.add(new PhysicsContact(bodyA, bodyB, pointA, pointB, normal, - raw[i + 13], raw[i + 14])); - } - return contacts; - } - - @Nonnull - @Override - public PhysicsJoint createFixedJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB) { - return createFixedJoint(bodyA, - bodyB, - anchorA.x, - anchorA.y, - anchorA.z, - anchorB.x, - anchorB.y, - anchorB.z); - } - - @Nonnull - @Override - public PhysicsJoint createFixedJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ) { - return createJoint(PhysicsJointType.FIXED, - bodyA, - bodyB, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - 0f, - 1f, - 0f, - 0f, - 0f, - 0f); - } - - @Nonnull - @Override - public PhysicsJoint createPointJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB) { - return createPointJoint(bodyA, - bodyB, - anchorA.x, - anchorA.y, - anchorA.z, - anchorB.x, - anchorB.y, - anchorB.z); - } - - @Nonnull - @Override - public PhysicsJoint createPointJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ) { - return createJoint(PhysicsJointType.POINT, - bodyA, - bodyB, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - 0f, - 1f, - 0f, - 0f, - 0f, - 0f); - } - - @Nonnull - @Override - public PhysicsJoint createHingeJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis) { - return createHingeJoint(bodyA, - bodyB, - anchorA.x, - anchorA.y, - anchorA.z, - anchorB.x, - anchorB.y, - anchorB.z, - axis.x, - axis.y, - axis.z); - } - - @Nonnull - @Override - public PhysicsJoint createHingeJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ) { - return createJoint(PhysicsJointType.HINGE, - bodyA, - bodyB, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - axisX, - axisY, - axisZ, - 0f, - 0f, - 0f); - } - - @Nonnull - @Override - public PhysicsJoint createSliderJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - @Nonnull Vector3f axis) { - return createSliderJoint(bodyA, - bodyB, - anchorA.x, - anchorA.y, - anchorA.z, - anchorB.x, - anchorB.y, - anchorB.z, - axis.x, - axis.y, - axis.z); - } - - @Nonnull - @Override - public PhysicsJoint createSliderJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ) { - return createJoint(PhysicsJointType.SLIDER, - bodyA, - bodyB, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - axisX, - axisY, - axisZ, - 0f, - 0f, - 0f); - } - - @Nonnull - @Override - public PhysicsJoint createSpringJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - @Nonnull Vector3f anchorA, - @Nonnull Vector3f anchorB, - float restLength, - float stiffness, - float damping) { - return createSpringJoint(bodyA, - bodyB, - anchorA.x, - anchorA.y, - anchorA.z, - anchorB.x, - anchorB.y, - anchorB.z, - restLength, - stiffness, - damping); - } - - @Nonnull - @Override - public PhysicsJoint createSpringJoint(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float restLength, - float stiffness, - float damping) { - return createJoint(PhysicsJointType.SPRING, - bodyA, - bodyB, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - 0f, - 1f, - 0f, - restLength, - stiffness, - damping); - } - - @Override - public void removeJoint(@Nonnull PhysicsJoint joint) { - if (closed) { - return; - } - if (!(joint instanceof RapierJoint rapierJoint)) { - return; - } - if (!rapierJoint.belongsTo(this)) { - throw new IllegalArgumentException("Rapier joint belongs to another space"); - } - if (!rapierJoint.isValidIn(this)) { - joints.remove(rapierJoint); - return; - } - RapierNative.removeJointNative(nativeSpaceHandle, rapierJoint.getJointHandle()); - joints.remove(rapierJoint); - rapierJoint.invalidate(this); - } - - @Nonnull - @Override - public List getJoints() { - return new ArrayList<>(joints); - } - - @Override - public int jointCount() { - return joints.size(); - } - - @Override - public void forEachJoint(@Nonnull Consumer consumer) { - for (RapierJoint joint : joints) { - consumer.accept(joint); - } - } - - long getNativeSpaceHandle() { - ensureOpen(); - return nativeSpaceHandle; - } - - boolean isClosed() { - return closed; - } - - @Override - public void close() { - if (closed) { - return; - } - closed = true; - for (RapierJoint joint : new ArrayList<>(joints)) { - joint.invalidate(this); - } - joints.clear(); - for (RapierBody body : new ArrayList<>(bodies)) { - body.detach(this); - } - bodies.clear(); - bodiesByHandle.clear(); - cleanable.clean(); - nativeSpaceHandle = 0L; - } - - private void removeAttachedJoints(@Nonnull RapierBody body) { - for (RapierJoint joint : new ArrayList<>(joints)) { - if (joint.getBodyA() != body && joint.getBodyB() != body) { - continue; - } - - RapierNative.removeJointNative(nativeSpaceHandle, joint.getJointHandle()); - joints.remove(joint); - joint.invalidate(this); - } - } - - private long addNativeBody(@Nonnull RapierBody body) { - if (body.getShapeType() == ShapeType.VOXELS) { - Vector3f position = body.getStoredPosition(); - Vector3f voxelSize = body.getVoxelSize(); - return RapierNative.addVoxelTerrainNative(nativeSpaceHandle, - voxelSize.x, - voxelSize.y, - voxelSize.z, - body.getVoxelCoordinates(), - position.x, - position.y, - position.z, - body.getStoredFriction(), - body.getStoredRestitution(), - body.getStoredCollisionGroup(), - body.getStoredCollisionMask()); - } - - Vector3f halfExtents = body.getBoxHalfExtents(); - if (halfExtents == null) { - halfExtents = new Vector3f(); - } - Vector3f position = body.getStoredPosition(); - Quaternionf rotation = body.getStoredRotation(); - Vector3f linearVelocity = body.getStoredLinearVelocity(); - Vector3f angularVelocity = body.getStoredAngularVelocity(); - - return RapierNative.addBodyNative(nativeSpaceHandle, - body.getShapeType().ordinal(), - halfExtents.x, - halfExtents.y, - halfExtents.z, - body.getSphereRadius(), - body.getHalfHeight(), - body.getShapeAxis().index(), - body.getStoredBodyType().ordinal(), - body.getStoredMass(), - position.x, - position.y, - position.z, - rotation.x, - rotation.y, - rotation.z, - rotation.w, - linearVelocity.x, - linearVelocity.y, - linearVelocity.z, - angularVelocity.x, - angularVelocity.y, - angularVelocity.z, - body.getStoredFriction(), - body.getStoredRestitution(), - body.getStoredLinearDamping(), - body.getStoredAngularDamping(), - body.getStoredSensor(), - body.getStoredCollisionGroup(), - body.getStoredCollisionMask(), - body.getStoredContinuousCollisionEnabled()); - } - - private PhysicsJoint createJoint(@Nonnull PhysicsJointType type, - @Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - float anchorAX, - float anchorAY, - float anchorAZ, - float anchorBX, - float anchorBY, - float anchorBZ, - float axisX, - float axisY, - float axisZ, - float restLength, - float stiffness, - float damping) { - ensureOpen(); - RapierBody rapierA = requireAttachedBody(bodyA); - RapierBody rapierB = requireAttachedBody(bodyB); - float normalizedAxisX = axisX; - float normalizedAxisY = axisY; - float normalizedAxisZ = axisZ; - float axisLengthSquared = axisX * axisX + axisY * axisY + axisZ * axisZ; - if (axisLengthSquared == 0f) { - normalizedAxisX = 0f; - normalizedAxisY = 1f; - normalizedAxisZ = 0f; - } else { - float inverseLength = (float) (1.0 / Math.sqrt(axisLengthSquared)); - normalizedAxisX *= inverseLength; - normalizedAxisY *= inverseLength; - normalizedAxisZ *= inverseLength; - } - long handle = RapierNative.addJointNative(nativeSpaceHandle, - type.ordinal(), - rapierA.getBodyHandle(), - rapierB.getBodyHandle(), - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - normalizedAxisX, - normalizedAxisY, - normalizedAxisZ, - restLength, - stiffness, - damping); - if (handle == 0L) { - throw new IllegalStateException("Rapier returned a null native joint handle"); - } - RapierJoint joint = new RapierJoint(this, type, rapierA, rapierB, handle, - anchorAX, - anchorAY, - anchorAZ, - anchorBX, - anchorBY, - anchorBZ, - normalizedAxisX, - normalizedAxisY, - normalizedAxisZ, - restLength, - stiffness, - damping); - joints.add(joint); - return joint; - } - - private RapierBody requireAttachedBody(@Nonnull PhysicsBody body) { - if (!(body instanceof RapierBody rapierBody)) { - throw new IllegalArgumentException("Body does not belong to rapier backend"); - } - if (!rapierBody.isAttachedTo(this)) { - if (rapierBody.isAttached()) { - throw new IllegalArgumentException("Rapier body belongs to another space"); - } - throw new IllegalStateException("Rapier joint bodies must be added to a space first"); - } - return rapierBody; - } - - private void ensureOpen() { - if (closed || nativeSpaceHandle == 0L) { - throw new IllegalStateException("Rapier space is closed"); - } - } - - private final class RapierSolverTuningCapability implements PhysicsSolverTuningCapability { - - @Override - public void setSolverTuning(@Nonnull PhysicsSolverTuning tuning) { - Objects.requireNonNull(tuning, "tuning"); - solverIterations = tuning.solverIterations(); - stabilizationIterations = tuning.stabilizationIterations(); - applyCurrentSolverTuning(); - } - } - - private final class RapierActivationTuningCapability implements PhysicsActivationTuningCapability { - - @Override - public void setActivationTuning(@Nonnull PhysicsActivationTuning tuning) { - Objects.requireNonNull(tuning, "tuning"); - setDynamicSleepTuning(tuning.linearSleepThreshold(), - tuning.angularSleepThreshold(), - tuning.timeUntilSleep()); - } - } - - private final class RapierVoxelTerrainCapability implements PhysicsVoxelTerrainCapability { - - @Nonnull - @Override - public PhysicsBody createVoxelTerrain(float voxelSizeX, - float voxelSizeY, - float voxelSizeZ, - @Nonnull int[] voxelCoordinates) { - return RapierSpace.this.createVoxelTerrain(voxelSizeX, - voxelSizeY, - voxelSizeZ, - voxelCoordinates); - } - - @Override - public void combineVoxelTerrains(@Nonnull PhysicsBody bodyA, - @Nonnull PhysicsBody bodyB, - int shiftX, - int shiftY, - int shiftZ) { - RapierSpace.this.combineVoxelTerrains(bodyA, bodyB, shiftX, shiftY, shiftZ); - } - } - - private final class RapierExtensionSettingsCapability implements PhysicsExtensionSettingsCapability { - - @Override - public void applyExtensionSettings(@Nonnull PhysicsCapabilityId capabilityId, - @Nonnull Map values) { - Objects.requireNonNull(capabilityId, "capabilityId"); - Objects.requireNonNull(values, "values"); - if (!RAPIER_SOLVER_EXTENSION_ID.equals(capabilityId)) { - return; - } - internalPgsIterations = parsePositive(values, - INTERNAL_PGS_ITERATIONS, - internalPgsIterations); - minIslandSize = parsePositive(values, - MIN_ISLAND_SIZE, - minIslandSize); - applyCurrentSolverTuning(); - } - - private int parsePositive(@Nonnull Map values, - @Nonnull String key, - int fallback) { - String value = values.get(key); - if (value == null) { - return fallback; - } - int parsed; - try { - parsed = Integer.parseInt(value); - } catch (NumberFormatException exception) { - throw new IllegalArgumentException("Rapier extension setting " + key - + " must be an integer", exception); - } - if (parsed < 1) { - throw new IllegalArgumentException("Rapier extension setting " + key - + " must be positive"); - } - return parsed; - } - } - - private static final class NativeSpaceCleanup implements Runnable { - - private final long nativeSpaceHandle; - - private NativeSpaceCleanup(long nativeSpaceHandle) { - this.nativeSpaceHandle = nativeSpaceHandle; - } - - @Override - public void run() { - RapierNative.destroySpaceNative(nativeSpaceHandle); - } - } -} diff --git a/impulse-rapier/src/main/resources/META-INF/services/dev.hytalemodding.impulse.api.PhysicsBackend b/impulse-rapier/src/main/resources/META-INF/services/dev.hytalemodding.impulse.api.PhysicsBackend deleted file mode 100644 index 5456be66..00000000 --- a/impulse-rapier/src/main/resources/META-INF/services/dev.hytalemodding.impulse.api.PhysicsBackend +++ /dev/null @@ -1 +0,0 @@ -dev.hytalemodding.impulse.rapier.RapierBackend diff --git a/impulse-rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierNativeBodyRemovalTest.java b/impulse-rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierNativeBodyRemovalTest.java deleted file mode 100644 index ee580de3..00000000 --- a/impulse-rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierNativeBodyRemovalTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package dev.hytalemodding.impulse.rapier; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.SpaceId; -import org.joml.Vector3f; -import org.junit.jupiter.api.Test; - -class RapierNativeBodyRemovalTest { - - @Test - void nativeBodyRemovalDropsAttachedJointHandles() { - RapierBackend backend = new RapierBackend(); - backend.init(); - RapierSpace space = (RapierSpace) backend.createSpace(new SpaceId(4)); - try { - PhysicsBody first = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - PhysicsBody second = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - space.addBody(first); - space.addBody(second); - space.createFixedJoint(first, second, new Vector3f(), new Vector3f()); - - assertEquals(1, RapierNative.jointHandleCountNative(space.getNativeSpaceHandle())); - - RapierNative.removeBodyNative(space.getNativeSpaceHandle(), - ((RapierBody) first).getBodyHandle()); - - assertEquals(0, RapierNative.jointHandleCountNative(space.getNativeSpaceHandle())); - assertEquals(0, space.getRuntimeStats().jointCount()); - } finally { - space.close(); - } - } -} diff --git a/impulse-rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierVoxelTerrainTest.java b/impulse-rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierVoxelTerrainTest.java deleted file mode 100644 index 93d386f9..00000000 --- a/impulse-rapier/src/test/java/dev/hytalemodding/impulse/rapier/RapierVoxelTerrainTest.java +++ /dev/null @@ -1,66 +0,0 @@ -package dev.hytalemodding.impulse.rapier; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import dev.hytalemodding.impulse.api.PhysicsBody; -import dev.hytalemodding.impulse.api.PhysicsSpace; -import dev.hytalemodding.impulse.api.SpaceId; -import dev.hytalemodding.impulse.api.capability.PhysicsVoxelTerrainCapability; -import org.junit.jupiter.api.Test; - -class RapierVoxelTerrainTest { - - @Test - void combineVoxelTerrainNativeReportsInvalidSpaceHandle() { - RapierNative.load(); - - assertFalse(RapierNative.combineVoxelTerrainNative(0L, 1L, 2L, 1, 0, 0)); - } - - @Test - void combineVoxelTerrainsRejectsNonVoxelBodies() { - RapierBackend backend = new RapierBackend(); - backend.init(); - PhysicsSpace space = backend.createSpace(new SpaceId(2)); - try { - PhysicsVoxelTerrainCapability voxelTerrain = space - .getCapability(PhysicsVoxelTerrainCapability.class) - .orElseThrow(); - PhysicsBody voxelBody = voxelTerrain.createVoxelTerrain(1.0f, - 1.0f, - 1.0f, - new int[] {0, 0, 0}); - PhysicsBody box = space.createBox(0.5f, 0.5f, 0.5f, 1.0f); - space.addBody(voxelBody); - space.addBody(box); - - assertThrows(IllegalArgumentException.class, - () -> voxelTerrain.combineVoxelTerrains(voxelBody, box, 1, 0, 0)); - } finally { - space.close(); - } - } - - @Test - void combineVoxelTerrainsRejectsSameBody() { - RapierBackend backend = new RapierBackend(); - backend.init(); - PhysicsSpace space = backend.createSpace(new SpaceId(3)); - try { - PhysicsVoxelTerrainCapability voxelTerrain = space - .getCapability(PhysicsVoxelTerrainCapability.class) - .orElseThrow(); - PhysicsBody voxelBody = voxelTerrain.createVoxelTerrain(1.0f, - 1.0f, - 1.0f, - new int[] {0, 0, 0}); - space.addBody(voxelBody); - - assertThrows(IllegalArgumentException.class, - () -> voxelTerrain.combineVoxelTerrains(voxelBody, voxelBody, 1, 0, 0)); - } finally { - space.close(); - } - } -} diff --git a/licenses/JOLT_PHYSICS_LICENSE b/licenses/JOLT_PHYSICS_LICENSE new file mode 100644 index 00000000..6546a990 --- /dev/null +++ b/licenses/JOLT_PHYSICS_LICENSE @@ -0,0 +1,13 @@ +Copyright 2021 Jorrit Rouwe + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/licenses/RAPIER_RUST_BACKEND_LICENSES b/licenses/RAPIER_RUST_BACKEND_LICENSES index 3e954033..b7a3aaad 100644 --- a/licenses/RAPIER_RUST_BACKEND_LICENSES +++ b/licenses/RAPIER_RUST_BACKEND_LICENSES @@ -1,4 +1,4 @@ -The experimental impulse-rapier backend links a first-party native shim against Rust crates. +The experimental impulse-backends/rapier backend links a first-party native shim against Rust crates. Direct Rust dependencies: @@ -10,4 +10,4 @@ Direct Rust dependencies: Repository: https://github.com/jni-rs/jni-rs License files: https://github.com/jni-rs/jni-rs/tree/master/licenses -Run `cargo metadata` in `impulse-rapier/src/main/rust` when producing a release artifact to audit transitive crate licenses. +Run `cargo metadata` in `impulse-backends/rapier/src/main/rust` when producing a release artifact to audit transitive crate licenses. diff --git a/scripts/ci/install-crucible-runtime.sh b/scripts/ci/install-crucible-runtime.sh deleted file mode 100755 index f49f1d46..00000000 --- a/scripts/ci/install-crucible-runtime.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -CRUCIBLE_VERSION="${CRUCIBLE_VERSION:-1.0.0}" -SERVER_VERSION="${1:-}" - -if [[ -z "${SERVER_VERSION}" ]]; then - SERVER_VERSION="$(awk -F= '$1 == "hytale_version" { print $2 }' "${ROOT_DIR}/gradle.properties")" -fi - -CRUCIBLE_CACHE_DIR="${HOME}/.gradle/caches/modules-2/files-2.1/com.ionforgelabs/crucible/${CRUCIBLE_VERSION}" -SOURCE_JAR="$(find "${CRUCIBLE_CACHE_DIR}" -name "crucible-${CRUCIBLE_VERSION}.jar" -type f | head -n 1)" - -if [[ -z "${SOURCE_JAR}" ]]; then - echo "Could not find Crucible ${CRUCIBLE_VERSION} in Gradle cache." >&2 - echo "Run ./gradlew :impulse-examples:dependencies --configuration compileClasspath first." >&2 - exit 1 -fi - -mkdir -p "${ROOT_DIR}/run/mods" - -python3 - "${SOURCE_JAR}" "${ROOT_DIR}/run/mods/crucible-${CRUCIBLE_VERSION}.jar" "${SERVER_VERSION}" <<'PY' -import json -import sys -import zipfile -from pathlib import Path - -source = Path(sys.argv[1]) -target = Path(sys.argv[2]) -server_version = sys.argv[3] - -with zipfile.ZipFile(source, "r") as src: - manifest = json.loads(src.read("manifest.json")) - manifest["ServerVersion"] = server_version - - with zipfile.ZipFile(target, "w") as dst: - for info in src.infolist(): - if info.filename == "manifest.json": - continue - dst.writestr(info, src.read(info.filename)) - - dst.writestr( - "manifest.json", - json.dumps(manifest, indent=2).encode("utf-8") + b"\n", - ) -PY - -echo "Installed patched Crucible ${CRUCIBLE_VERSION} for Hytale ${SERVER_VERSION}:" -echo "${ROOT_DIR}/run/mods/crucible-${CRUCIBLE_VERSION}.jar" diff --git a/settings.gradle.kts b/settings.gradle.kts index 650ffe02..be4b1eb3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,18 +17,16 @@ dependencyResolutionManagement { maven { url = uri("impulse-core/build/generated-sources-m2") } - // Crucible Maven repository. - maven { - url = uri("https://gitlab.com/api/v4/projects/82033924/packages/maven") - } } } rootProject.name = "impulse" -include("impulse-api") -include("impulse-native-loader") -include("impulse-bullet") -include("impulse-rapier") +include(":impulse-backends:api") +include(":impulse-backends:native-loader") +include(":impulse-backends:jolt") +include(":impulse-backends:rapier") +include(":impulse-builtins:control") include("impulse-core") include("impulse-examples") +include("impulse-early-plugin")