From c33ac09d70fd563c84b23bde9da1f4695069eb95 Mon Sep 17 00:00:00 2001 From: devin Date: Fri, 16 Oct 2026 18:09:16 +0200 Subject: [PATCH 1/2] Add in sublevel support --- build.gradle | 9 ++++ gradle.properties | 7 +-- .../kotlin/de/devin/cbbees/CreateBuzzyBeez.kt | 3 ++ .../cbbees/compat/sable/CompanionSubLevels.kt | 36 +++++++++++++ .../devin/cbbees/compat/sable/FlightSpace.kt | 23 ++++++++ .../devin/cbbees/compat/sable/RenderSpace.kt | 18 +++++++ .../devin/cbbees/compat/sable/SableCompat.kt | 34 ++++++++++++ .../compat/sable/SubLevelFlightSpace.kt | 38 +++++++++++++ .../cbbees/compat/sable/SubLevelFrame.kt | 35 ++++++++++++ .../compat/sable/SubLevelNetworkTracker.kt | 53 +++++++++++++++++++ .../cbbees/compat/sable/SubLevelProvider.kt | 18 +++++++ .../compat/sable/SubLevelRenderSpace.kt | 19 +++++++ .../content/bee/client/BeeWorldRenderer.kt | 4 +- .../content/bee/flight/CheckpointActions.kt | 5 +- .../content/bee/flight/ClientBeeFlightData.kt | 37 +++++++------ .../cbbees/content/bee/flight/FlightPlan.kt | 29 ++++++---- .../content/bee/flight/FlightPlanComputer.kt | 37 +++++++------ .../content/bee/server/ServerBeeManager.kt | 24 ++++----- .../deployer/SchematicDeployerBlockEntity.kt | 10 +++- .../cbbees/content/domain/GlobalJobPool.kt | 7 +-- .../content/domain/TransportDispatcher.kt | 3 +- .../cbbees/content/domain/beehive/BeeHive.kt | 8 +-- .../content/domain/network/BeeNetwork.kt | 3 +- .../domain/network/BeeNetworkManager.kt | 27 ++++++---- .../network/topology/DefaultAnchorTopology.kt | 7 ++- .../schematics/client/ConstructionRenderer.kt | 41 +++++++++----- .../client/GhostSchematicRenderer.kt | 14 ++++- .../client/SchematicHoverPreview.kt | 31 ++++++----- .../cbbees/gametest/flight/FlightPlanTests.kt | 13 +++-- .../cbbees/network/BeeFlightPlanPacket.kt | 2 + .../cbbees/network/DeployerSettingsPacket.kt | 4 +- .../templates/META-INF/neoforge.mods.toml | 9 +++- 32 files changed, 486 insertions(+), 122 deletions(-) create mode 100644 src/main/kotlin/de/devin/cbbees/compat/sable/CompanionSubLevels.kt create mode 100644 src/main/kotlin/de/devin/cbbees/compat/sable/FlightSpace.kt create mode 100644 src/main/kotlin/de/devin/cbbees/compat/sable/RenderSpace.kt create mode 100644 src/main/kotlin/de/devin/cbbees/compat/sable/SableCompat.kt create mode 100644 src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelFlightSpace.kt create mode 100644 src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelFrame.kt create mode 100644 src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelNetworkTracker.kt create mode 100644 src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelProvider.kt create mode 100644 src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelRenderSpace.kt diff --git a/build.gradle b/build.gradle index 58a753b..71bfab6 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,11 @@ repositories { } maven { url = 'https://dl.cloudsmith.io/public/geckolib3/geckolib/maven/' } maven { url = "https://maven.blamejared.com/" } // JEI + maven { + name = "Sable" + url = uri("https://maven.ryanhcode.dev/releases") + content { includeGroup "dev.ryanhcode.sable-companion" } + } } base { @@ -132,6 +137,8 @@ dependencies { implementation("software.bernie.geckolib:geckolib-neoforge-1.21.1:4.7.1") + compileOnly("dev.ryanhcode.sable-companion:sable-companion-common-${minecraft_version}:${sable_companion_version}") + // WebP thumbnail support (pure Java ImageIO plugin) // implementation = compile classpath, jarJar = bundled in prod, additionalRuntimeClasspath = dev runtime implementation "com.twelvemonkeys.imageio:imageio-webp:3.12.0" @@ -234,6 +241,7 @@ if (System.getenv("CURSEFORGE_TOKEN")) { mainFile.addRequirement('geckolib') mainFile.addOptional('curios') mainFile.addOptional('jei') + mainFile.addOptional('sable') } } @@ -253,6 +261,7 @@ if (System.getenv("MODRINTH_TOKEN")) { required.project 'geckolib' optional.project 'curios' optional.project 'jei' + optional.project 'sable' } } } diff --git a/gradle.properties b/gradle.properties index 757e17a..f067ea1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ minecraft_version=1.21.1 # as they do not follow standard versioning conventions. minecraft_version_range=[1.21.1] # The Neo version must agree with the Minecraft version to get a valid artifact -neo_version=21.1.206 +neo_version=21.1.262 # The Neo version range can use any version of Neo as bounds neo_version_range=[21,) # The loader version range can only use the major version of FML as bounds @@ -21,8 +21,8 @@ loader_version_range=[5.3,) parchment_minecraft_version=1.21.1 parchment_mappings_version=2024.11.17 ## Create -create_version=6.0.9-215 -ponder_version=1.0.81 +create_version=6.0.10-281 +ponder_version=1.0.87 flywheel_version=1.0.6 registrate_version=MC1.21-1.3.0+67 #jei @@ -30,6 +30,7 @@ jei_version=19.21.0.247 #curios curios_minecraft_version=1.21.1 curios_version=9.2.2 +sable_companion_version=1.6.0 ## Mod Properties # The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63} # Must match the String constant located in the main mod class annotated with @Mod. diff --git a/src/main/kotlin/de/devin/cbbees/CreateBuzzyBeez.kt b/src/main/kotlin/de/devin/cbbees/CreateBuzzyBeez.kt index 96a7e4b..9de14a6 100644 --- a/src/main/kotlin/de/devin/cbbees/CreateBuzzyBeez.kt +++ b/src/main/kotlin/de/devin/cbbees/CreateBuzzyBeez.kt @@ -4,6 +4,8 @@ import com.simibubi.create.foundation.data.CreateRegistrate import com.simibubi.create.foundation.item.ItemDescription import com.simibubi.create.foundation.item.KineticStats import com.simibubi.create.foundation.item.TooltipModifier +import de.devin.cbbees.compat.sable.SableCompat +import de.devin.cbbees.compat.sable.SubLevelNetworkTracker import de.devin.cbbees.blocks.AllBlocks import de.devin.cbbees.config.CBBeesClientConfig import de.devin.cbbees.config.CBBeesConfig @@ -182,6 +184,7 @@ object CreateBuzzyBeez { // Register server-side event handlers on the NeoForge event bus NeoForge.EVENT_BUS.register(CCRServerEvents::class.java) NeoForge.EVENT_BUS.register(PlayerTickEvent()) + if (SableCompat.isLoaded) NeoForge.EVENT_BUS.register(SubLevelNetworkTracker::class.java) } fun asResource(path: String): ResourceLocation { diff --git a/src/main/kotlin/de/devin/cbbees/compat/sable/CompanionSubLevels.kt b/src/main/kotlin/de/devin/cbbees/compat/sable/CompanionSubLevels.kt new file mode 100644 index 0000000..0f4304a --- /dev/null +++ b/src/main/kotlin/de/devin/cbbees/compat/sable/CompanionSubLevels.kt @@ -0,0 +1,36 @@ +package de.devin.cbbees.compat.sable + +import dev.ryanhcode.sable.companion.SableCompanion +import dev.ryanhcode.sable.companion.math.Pose3dc +import net.minecraft.core.BlockPos +import net.minecraft.core.Position +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import org.joml.Quaterniond +import org.joml.Vector3d +import java.util.UUID + +object CompanionSubLevels : SubLevelProvider { + + private val companion: SableCompanion get() = SableCompanion.INSTANCE + + override fun subLevelId(level: Level, pos: BlockPos): UUID? = + companion.getContaining(level, pos)?.uniqueId + + override fun toWorld(level: Level, pos: Vec3): Vec3 = + companion.projectOutOfSubLevel(level, pos as Position) + + override fun worldDistanceSqr(level: Level, a: Vec3, b: Vec3): Double = + companion.distanceSquaredWithSubLevels(level, a, b) + + fun clientFrame(pos: BlockPos, partialTick: Float): SubLevelFrame? = + companion.getContainingClient(pos)?.let { frameOf(it.uniqueId, it.renderPose(partialTick)) } + + private fun frameOf(id: UUID, pose: Pose3dc) = SubLevelFrame( + id, + Vector3d(pose.position()), + Quaterniond(pose.orientation()), + Vector3d(pose.rotationPoint()), + Vector3d(pose.scale()) + ) +} diff --git a/src/main/kotlin/de/devin/cbbees/compat/sable/FlightSpace.kt b/src/main/kotlin/de/devin/cbbees/compat/sable/FlightSpace.kt new file mode 100644 index 0000000..6ffbdf4 --- /dev/null +++ b/src/main/kotlin/de/devin/cbbees/compat/sable/FlightSpace.kt @@ -0,0 +1,23 @@ +package de.devin.cbbees.compat.sable + +import net.minecraft.core.BlockPos +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import org.joml.Quaternionf + +interface FlightSpace { + fun crossings(level: Level, positions: List): Set + fun pose(from: BlockPos, to: BlockPos, t: Double, offset: Vec3, partialTick: Float): FlightPose + fun heading(from: BlockPos, to: BlockPos): Vec3 +} + +data class FlightPose(val position: Vec3, val rotation: Quaternionf?) + +object FlatFlightSpace : FlightSpace { + override fun crossings(level: Level, positions: List): Set = emptySet() + + override fun pose(from: BlockPos, to: BlockPos, t: Double, offset: Vec3, partialTick: Float) = + FlightPose(Vec3.atCenterOf(from).lerp(Vec3.atCenterOf(to), t).add(offset), null) + + override fun heading(from: BlockPos, to: BlockPos): Vec3 = Vec3.atCenterOf(to).subtract(Vec3.atCenterOf(from)) +} diff --git a/src/main/kotlin/de/devin/cbbees/compat/sable/RenderSpace.kt b/src/main/kotlin/de/devin/cbbees/compat/sable/RenderSpace.kt new file mode 100644 index 0000000..a084075 --- /dev/null +++ b/src/main/kotlin/de/devin/cbbees/compat/sable/RenderSpace.kt @@ -0,0 +1,18 @@ +package de.devin.cbbees.compat.sable + +import com.mojang.blaze3d.vertex.PoseStack +import net.minecraft.core.BlockPos +import net.minecraft.world.phys.Vec3 +import java.util.function.Consumer + +interface RenderSpace { + fun draw(poseStack: PoseStack, anchor: BlockPos, camera: Vec3, partialTick: Float, render: Consumer) + fun toLocal(anchor: BlockPos, world: Vec3, partialTick: Float): Vec3 +} + +object FlatRenderSpace : RenderSpace { + override fun draw(poseStack: PoseStack, anchor: BlockPos, camera: Vec3, partialTick: Float, render: Consumer) = + render.accept(camera) + + override fun toLocal(anchor: BlockPos, world: Vec3, partialTick: Float): Vec3 = world +} diff --git a/src/main/kotlin/de/devin/cbbees/compat/sable/SableCompat.kt b/src/main/kotlin/de/devin/cbbees/compat/sable/SableCompat.kt new file mode 100644 index 0000000..5d01b35 --- /dev/null +++ b/src/main/kotlin/de/devin/cbbees/compat/sable/SableCompat.kt @@ -0,0 +1,34 @@ +package de.devin.cbbees.compat.sable + +import net.minecraft.core.BlockPos +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +object SableCompat { + + private const val COMPANION_CLASS = "dev.ryanhcode.sable.companion.SableCompanion" + + val isLoaded: Boolean = isPresent(COMPANION_CLASS) + + val provider: SubLevelProvider = if (isLoaded) CompanionSubLevels else NoSubLevels + + val flight: FlightSpace = if (isLoaded) SubLevelFlightSpace else FlatFlightSpace + + val render: RenderSpace = if (isLoaded) SubLevelRenderSpace else FlatRenderSpace + + private fun isPresent(className: String) = runCatching { + Class.forName(className, false, SableCompat::class.java.classLoader) + }.isSuccess +} + +fun Level.subLevelId(pos: BlockPos): UUID? = SableCompat.provider.subLevelId(this, pos) + +fun Level.toWorld(pos: Vec3): Vec3 = SableCompat.provider.toWorld(this, pos) + +fun Level.toWorld(pos: BlockPos): Vec3 = toWorld(Vec3.atCenterOf(pos)) + +fun Level.worldDistanceSqr(a: Vec3, b: Vec3): Double = SableCompat.provider.worldDistanceSqr(this, a, b) + +fun Level.worldDistanceSqr(a: BlockPos, b: BlockPos): Double = + worldDistanceSqr(Vec3.atCenterOf(a), Vec3.atCenterOf(b)) diff --git a/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelFlightSpace.kt b/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelFlightSpace.kt new file mode 100644 index 0000000..d6cc34d --- /dev/null +++ b/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelFlightSpace.kt @@ -0,0 +1,38 @@ +package de.devin.cbbees.compat.sable + +import net.minecraft.core.BlockPos +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 + +object SubLevelFlightSpace : FlightSpace { + + override fun crossings(level: Level, positions: List): Set { + val spaces = positions.map { CompanionSubLevels.subLevelId(level, it) } + return (1 until spaces.size).filter { spaces[it - 1] != spaces[it] }.map { it - 1 }.toSet() + } + + override fun pose(from: BlockPos, to: BlockPos, t: Double, offset: Vec3, partialTick: Float): FlightPose { + val start = Vec3.atCenterOf(from) + val end = Vec3.atCenterOf(to) + val fromFrame = CompanionSubLevels.clientFrame(from, partialTick) + val toFrame = if (to == from) fromFrame else CompanionSubLevels.clientFrame(to, partialTick) + + if (fromFrame?.id == toFrame?.id) { + val local = start.lerp(end, t).add(offset) + return FlightPose(fromFrame?.project(local) ?: local, fromFrame?.rotation) + } + + val worldStart = fromFrame?.project(start) ?: start + val worldEnd = toFrame?.project(end) ?: end + return FlightPose(worldStart.lerp(worldEnd, t).add(offset), null) + } + + override fun heading(from: BlockPos, to: BlockPos): Vec3 { + val start = Vec3.atCenterOf(from) + val end = Vec3.atCenterOf(to) + val fromFrame = CompanionSubLevels.clientFrame(from, 1f) + val toFrame = CompanionSubLevels.clientFrame(to, 1f) + if (fromFrame?.id == toFrame?.id) return end.subtract(start) + return (toFrame?.project(end) ?: end).subtract(fromFrame?.project(start) ?: start) + } +} diff --git a/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelFrame.kt b/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelFrame.kt new file mode 100644 index 0000000..c20c2f9 --- /dev/null +++ b/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelFrame.kt @@ -0,0 +1,35 @@ +package de.devin.cbbees.compat.sable + +import com.mojang.blaze3d.vertex.PoseStack +import net.minecraft.world.phys.Vec3 +import org.joml.Quaterniond +import org.joml.Quaternionf +import org.joml.Vector3d +import java.util.UUID + +class SubLevelFrame( + val id: UUID, + private val position: Vector3d, + private val orientation: Quaterniond, + private val rotationPoint: Vector3d, + private val scale: Vector3d +) { + val rotation: Quaternionf get() = Quaternionf(orientation) + + fun project(local: Vec3): Vec3 { + val world = orientation.transform(Vector3d(local.x, local.y, local.z).sub(rotationPoint).mul(scale)).add(position) + return Vec3(world.x, world.y, world.z) + } + + fun unproject(world: Vec3): Vec3 { + val local = orientation.transformInverse(Vector3d(world.x, world.y, world.z).sub(position)).div(scale).add(rotationPoint) + return Vec3(local.x, local.y, local.z) + } + + fun applyTo(poseStack: PoseStack, camera: Vec3): Vec3 { + poseStack.translate(position.x - camera.x, position.y - camera.y, position.z - camera.z) + poseStack.mulPose(rotation) + poseStack.scale(scale.x.toFloat(), scale.y.toFloat(), scale.z.toFloat()) + return Vec3(rotationPoint.x, rotationPoint.y, rotationPoint.z) + } +} diff --git a/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelNetworkTracker.kt b/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelNetworkTracker.kt new file mode 100644 index 0000000..044cb28 --- /dev/null +++ b/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelNetworkTracker.kt @@ -0,0 +1,53 @@ +package de.devin.cbbees.compat.sable + +import de.devin.cbbees.CreateBuzzyBeez +import de.devin.cbbees.content.domain.beehive.PortableBeeHive +import de.devin.cbbees.content.domain.network.BeeNetwork +import de.devin.cbbees.content.domain.network.ServerBeeNetworkManager +import de.devin.cbbees.util.ServerSide +import net.neoforged.bus.api.SubscribeEvent +import net.neoforged.neoforge.event.tick.ServerTickEvent + +@ServerSide +object SubLevelNetworkTracker { + + private const val INTERVAL_TICKS = 10 + + @SubscribeEvent + @JvmStatic + fun onServerTick(event: ServerTickEvent.Post) { + if (event.server.tickCount % INTERVAL_TICKS != 0) return + val split = splitDisconnected() + val merged = mergeReconnected() + if (split || merged) ServerBeeNetworkManager.rebuildIndexes() + } + + private fun splitDisconnected(): Boolean = + movingNetworks().map { network -> + ServerBeeNetworkManager.splitIfDisconnected(network).also { didSplit -> + if (didSplit) CreateBuzzyBeez.LOGGER.info("[NET] Network ${network.id} split after its sub-level moved") + } + }.any { it } + + private fun mergeReconnected(): Boolean { + var merged = false + movingNetworks().forEach { network -> + if (network !in ServerBeeNetworkManager.getNetworks()) return@forEach + val anchors = network.components.filter { it.isAnchor() } + ServerBeeNetworkManager.getNetworks() + .filter { other -> other !== network && !other.isPortable() && anchors.any(other::canConnect) } + .forEach { other -> + ServerBeeNetworkManager.mergeNetworks(network, other) + CreateBuzzyBeez.LOGGER.info("[NET] Network ${other.id} merged into ${network.id} after a sub-level moved into range") + merged = true + } + } + return merged + } + + private fun movingNetworks() = ServerBeeNetworkManager.getNetworks().filter { network -> + !network.isPortable() && network.components.any { it.world.subLevelId(it.pos) != null } + } + + private fun BeeNetwork.isPortable() = components.any { it is PortableBeeHive } +} diff --git a/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelProvider.kt b/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelProvider.kt new file mode 100644 index 0000000..7270e12 --- /dev/null +++ b/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelProvider.kt @@ -0,0 +1,18 @@ +package de.devin.cbbees.compat.sable + +import net.minecraft.core.BlockPos +import net.minecraft.world.level.Level +import net.minecraft.world.phys.Vec3 +import java.util.UUID + +interface SubLevelProvider { + fun subLevelId(level: Level, pos: BlockPos): UUID? + fun toWorld(level: Level, pos: Vec3): Vec3 + fun worldDistanceSqr(level: Level, a: Vec3, b: Vec3): Double +} + +object NoSubLevels : SubLevelProvider { + override fun subLevelId(level: Level, pos: BlockPos): UUID? = null + override fun toWorld(level: Level, pos: Vec3): Vec3 = pos + override fun worldDistanceSqr(level: Level, a: Vec3, b: Vec3): Double = a.distanceToSqr(b) +} diff --git a/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelRenderSpace.kt b/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelRenderSpace.kt new file mode 100644 index 0000000..9ff7109 --- /dev/null +++ b/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelRenderSpace.kt @@ -0,0 +1,19 @@ +package de.devin.cbbees.compat.sable + +import com.mojang.blaze3d.vertex.PoseStack +import net.minecraft.core.BlockPos +import net.minecraft.world.phys.Vec3 +import java.util.function.Consumer + +object SubLevelRenderSpace : RenderSpace { + + override fun draw(poseStack: PoseStack, anchor: BlockPos, camera: Vec3, partialTick: Float, render: Consumer) { + val frame = CompanionSubLevels.clientFrame(anchor, partialTick) ?: return render.accept(camera) + poseStack.pushPose() + render.accept(frame.applyTo(poseStack, camera)) + poseStack.popPose() + } + + override fun toLocal(anchor: BlockPos, world: Vec3, partialTick: Float): Vec3 = + CompanionSubLevels.clientFrame(anchor, partialTick)?.unproject(world) ?: world +} diff --git a/src/main/kotlin/de/devin/cbbees/content/bee/client/BeeWorldRenderer.kt b/src/main/kotlin/de/devin/cbbees/content/bee/client/BeeWorldRenderer.kt index d3d0911..f5efe25 100644 --- a/src/main/kotlin/de/devin/cbbees/content/bee/client/BeeWorldRenderer.kt +++ b/src/main/kotlin/de/devin/cbbees/content/bee/client/BeeWorldRenderer.kt @@ -63,7 +63,8 @@ object BeeWorldRenderer { val time = System.nanoTime() / 1_000_000_000.0f flightBees.forEach { bee -> - val pos = bee.lerpPos(partialTick) + val pose = bee.lerpPose(partialTick) + val pos = pose.position val dx = pos.x - camPos.x val dy = pos.y - camPos.y val dz = pos.z - camPos.z @@ -76,6 +77,7 @@ object BeeWorldRenderer { poseStack.pushPose() poseStack.translate(dx, dy, dz) + pose.rotation?.let(poseStack::mulPose) poseStack.mulPose(Axis.YP.rotationDegrees(180f - bee.yRot())) renderModel(poseStack, bufferSource, modelRes, textureRes, time) diff --git a/src/main/kotlin/de/devin/cbbees/content/bee/flight/CheckpointActions.kt b/src/main/kotlin/de/devin/cbbees/content/bee/flight/CheckpointActions.kt index dfee95b..8379d7e 100644 --- a/src/main/kotlin/de/devin/cbbees/content/bee/flight/CheckpointActions.kt +++ b/src/main/kotlin/de/devin/cbbees/content/bee/flight/CheckpointActions.kt @@ -223,11 +223,8 @@ class CheckForNextWork : CheckpointAction { bee.planStartTick = level.gameTime bee.currentCheckpointIndex = 0 if (plan.checkpoints.size > 1) { - val travel = FlightPlan.travelTicks( - plan.checkpoints[0].pos, plan.checkpoints[1].pos, plan.speed - ) bee.currentCheckpointIndex = 1 - bee.nextCheckpointArrivalTick = level.gameTime + travel + bee.nextCheckpointArrivalTick = level.gameTime + plan.segmentTicks[1] } ServerBeeManager.broadcastFlightPlan(bee, plan, clientStartIndex = 0) } diff --git a/src/main/kotlin/de/devin/cbbees/content/bee/flight/ClientBeeFlightData.kt b/src/main/kotlin/de/devin/cbbees/content/bee/flight/ClientBeeFlightData.kt index 26d98a7..9f2c95e 100644 --- a/src/main/kotlin/de/devin/cbbees/content/bee/flight/ClientBeeFlightData.kt +++ b/src/main/kotlin/de/devin/cbbees/content/bee/flight/ClientBeeFlightData.kt @@ -1,5 +1,7 @@ package de.devin.cbbees.content.bee.flight +import de.devin.cbbees.compat.sable.FlightPose +import de.devin.cbbees.compat.sable.SableCompat import de.devin.cbbees.content.bee.server.BeeType import net.minecraft.core.BlockPos import net.minecraft.world.phys.Vec3 @@ -26,13 +28,12 @@ class ClientBeeFlightData( /** Unused — kept for packet compat. Sync uses startIndex instead. */ elapsedNanoOffset: Long = 0, ) { - private val positions: List = checkpoints.map { Vec3.atCenterOf(it.pos) } - private val arrivalNanos: LongArray = LongArray(checkpoints.size).also { arr -> var cumulative = 0L arr[0] = 0 for (i in 1 until checkpoints.size) { - val travelTicks = FlightPlan.travelTicks(checkpoints[i - 1].pos, checkpoints[i].pos, speed) + val travelTicks = checkpoints[i].travelTicks.takeIf { it > 0 } + ?: FlightPlan.travelTicks(checkpoints[i - 1].pos, checkpoints[i].pos, speed) cumulative += (travelTicks + checkpoints[i - 1].pauseTicks) * NANOS_PER_TICK arr[i] = cumulative } @@ -145,21 +146,23 @@ class ClientBeeFlightData( } /** - * Exact position at this instant. Called every render frame. + * Exact pose at this instant. Called every render frame. * Reads [System.nanoTime] directly — smooth at any FPS/TPS. */ - fun lerpPos(@Suppress("UNUSED_PARAMETER") partialTick: Float): Vec3 { - if (checkpoints.size < 2) return positions.firstOrNull() ?: Vec3.ZERO - return computePositionAtNano(elapsedNano()) + fun lerpPose(partialTick: Float): FlightPose { + if (checkpoints.isEmpty()) return FlightPose(Vec3.ZERO, null) + if (checkpoints.size < 2) return resolve(0, 0, 0.0, Vec3.ZERO, partialTick) + return computePoseAtNano(elapsedNano(), partialTick) } fun yRot(): Float = currentYRot // ── Position computation ── - private fun computePositionAtNano(nano: Long): Vec3 { + private fun computePoseAtNano(nano: Long, partialTick: Float): FlightPose { val (segIndex, segStartNano, segEndNano) = findSegment(nano) - if (segIndex >= positions.size - 1) return positions.last().add(flightOffset(nano)).add(separationOffset()) + val offset = flightOffset(nano).add(separationOffset()) + if (segIndex >= checkpoints.size - 1) return resolve(checkpoints.size - 1, checkpoints.size - 1, 0.0, offset, partialTick) val pauseNano = checkpoints[segIndex].pauseTicks.toLong() * NANOS_PER_TICK val travelStartNano = segStartNano + pauseNano @@ -170,11 +173,12 @@ class ClientBeeFlightData( val t = easeIn(rawT).toDouble() - return positions[segIndex].lerp(positions[segIndex + 1], t) - .add(flightOffset(nano)) - .add(separationOffset()) + return resolve(segIndex, segIndex + 1, t, offset, partialTick) } + private fun resolve(from: Int, to: Int, t: Double, offset: Vec3, partialTick: Float): FlightPose = + SableCompat.flight.pose(checkpoints[from].pos, checkpoints[to].pos, t, offset, partialTick) + private fun findSegment(nano: Long): Triple { for (i in 1 until arrivalNanos.size) { if (nano < arrivalNanos[i]) return Triple(i - 1, arrivalNanos[i - 1], arrivalNanos[i]) @@ -185,10 +189,10 @@ class ClientBeeFlightData( private fun computeSegmentYRot(): Float { val (segIndex, _, _) = findSegment(elapsedNano()) - val from = positions.getOrNull(segIndex) ?: return currentYRot - val to = positions.getOrNull(segIndex + 1) ?: return currentYRot - val dx = to.x - from.x - val dz = to.z - from.z + if (segIndex + 1 >= checkpoints.size) return currentYRot + val heading = SableCompat.flight.heading(checkpoints[segIndex].pos, checkpoints[segIndex + 1].pos) + val dx = heading.x + val dz = heading.z return if (dx * dx + dz * dz < 0.01) currentYRot else (atan2(-dx, dz) * (180.0 / Math.PI)).toFloat() } @@ -229,4 +233,5 @@ data class ClientCheckpoint( val pauseTicks: Int = 0, /** If true, the client holds the bee at this checkpoint until the server confirms completion. */ val awaitConfirm: Boolean = false, + val travelTicks: Long = 0, ) diff --git a/src/main/kotlin/de/devin/cbbees/content/bee/flight/FlightPlan.kt b/src/main/kotlin/de/devin/cbbees/content/bee/flight/FlightPlan.kt index 9700449..0c84f6a 100644 --- a/src/main/kotlin/de/devin/cbbees/content/bee/flight/FlightPlan.kt +++ b/src/main/kotlin/de/devin/cbbees/content/bee/flight/FlightPlan.kt @@ -1,11 +1,14 @@ package de.devin.cbbees.content.bee.flight +import de.devin.cbbees.compat.sable.worldDistanceSqr import de.devin.cbbees.content.bee.server.BeeType import de.devin.cbbees.content.bee.server.ServerBeeData import net.minecraft.core.BlockPos import net.minecraft.server.level.ServerLevel +import net.minecraft.world.level.Level import net.minecraft.world.phys.Vec3 import java.util.UUID +import kotlin.math.sqrt /** * A bee's full mission represented as an ordered list of [Checkpoint] waypoints. @@ -32,36 +35,40 @@ data class FlightPlan( val type: BeeType, val speed: Float, val checkpoints: List, + val segmentTicks: List, ) { /** Estimated total flight ticks (sum of distances / speed + pause ticks). */ val estimatedDurationTicks: Long - get() = checkpoints.zipWithNext().sumOf { (a, b) -> - travelTicks(a.pos, b.pos, speed) + a.clientPauseTicks - } + get() = (1 until checkpoints.size).sumOf { segmentTicks[it] + checkpoints[it - 1].clientPauseTicks } /** * Pre-computes cumulative arrival ticks for each checkpoint. - * Used by both server (for checkpoint advancement) and client (for position interpolation). - * Guarantees identical timing on both sides. + * Used by the server for checkpoint advancement; the client receives [segmentTicks] + * directly, which guarantees identical timing on both sides. */ fun computeArrivalTicks(): LongArray = LongArray(checkpoints.size).also { arr -> var cumulative = 0L - arr[0] = 0 for (i in 1 until checkpoints.size) { - val travel = travelTicks(checkpoints[i - 1].pos, checkpoints[i].pos, speed) - cumulative += travel + checkpoints[i - 1].clientPauseTicks + cumulative += segmentTicks[i] + checkpoints[i - 1].clientPauseTicks arr[i] = cumulative } } companion object { + fun of(beeId: UUID, type: BeeType, speed: Float, checkpoints: List, level: Level?) = FlightPlan( + beeId, type, speed, checkpoints, + checkpoints.indices.map { i -> + if (i == 0) 0L else travelTicks(checkpoints[i - 1].pos, checkpoints[i].pos, speed, level) + } + ) + /** * Euclidean travel time between two block positions at the given speed. * Single source of truth — used by server, client, and duration estimates. */ - fun travelTicks(from: BlockPos, to: BlockPos, speed: Float): Long { - val dist = Vec3.atCenterOf(from).distanceTo(Vec3.atCenterOf(to)) - return (dist / speed).toLong().coerceAtLeast(1) + fun travelTicks(from: BlockPos, to: BlockPos, speed: Float, level: Level? = null): Long { + val distSqr = level?.worldDistanceSqr(from, to) ?: Vec3.atCenterOf(from).distanceToSqr(Vec3.atCenterOf(to)) + return (sqrt(distSqr) / speed).toLong().coerceAtLeast(1) } } } diff --git a/src/main/kotlin/de/devin/cbbees/content/bee/flight/FlightPlanComputer.kt b/src/main/kotlin/de/devin/cbbees/content/bee/flight/FlightPlanComputer.kt index ce2b337..b133942 100644 --- a/src/main/kotlin/de/devin/cbbees/content/bee/flight/FlightPlanComputer.kt +++ b/src/main/kotlin/de/devin/cbbees/content/bee/flight/FlightPlanComputer.kt @@ -1,5 +1,6 @@ package de.devin.cbbees.content.bee.flight +import de.devin.cbbees.compat.sable.SableCompat import de.devin.cbbees.CreateBuzzyBeez import de.devin.cbbees.content.bee.flight.FlightPlanComputer.computeAsync import de.devin.cbbees.content.bee.flight.FlightPlanComputer.computeTransportAsync @@ -67,12 +68,12 @@ object FlightPlanComputer { return } - val collisionSnapshot = snapshotCollisions(rawCheckpoints.map { it.pos }, level) + val crossings = spaceCrossings(rawCheckpoints, level) + val collisionSnapshot = snapshotCollisions(rawCheckpoints.map { it.pos }, crossings, level) executor.submit { - val finalCheckpoints = insertObstacleWaypointsFromSnapshot(rawCheckpoints, collisionSnapshot, level) - val plan = FlightPlan(bee.id, bee.type, DEFAULT_SPEED, finalCheckpoints) - level.server.execute { onComplete(plan) } + val finalCheckpoints = insertObstacleWaypointsFromSnapshot(rawCheckpoints, collisionSnapshot, crossings) + level.server.execute { onComplete(FlightPlan.of(bee.id, bee.type, DEFAULT_SPEED, finalCheckpoints, level)) } } } @@ -86,12 +87,12 @@ object FlightPlanComputer { onComplete: (FlightPlan) -> Unit, ) { val rawCheckpoints = buildRawTransportCheckpoints(bee, task) - val collisionSnapshot = snapshotCollisions(rawCheckpoints.map { it.pos }, level) + val crossings = spaceCrossings(rawCheckpoints, level) + val collisionSnapshot = snapshotCollisions(rawCheckpoints.map { it.pos }, crossings, level) executor.submit { - val finalCheckpoints = insertObstacleWaypointsFromSnapshot(rawCheckpoints, collisionSnapshot, level) - val plan = FlightPlan(bee.id, BeeType.TRANSPORT, DEFAULT_SPEED, finalCheckpoints) - level.server.execute { onComplete(plan) } + val finalCheckpoints = insertObstacleWaypointsFromSnapshot(rawCheckpoints, collisionSnapshot, crossings) + level.server.execute { onComplete(FlightPlan.of(bee.id, BeeType.TRANSPORT, DEFAULT_SPEED, finalCheckpoints, level)) } } } @@ -99,10 +100,11 @@ object FlightPlanComputer { * Snapshots block collision state along all straight-line segments between positions. * Called on the server thread. Returns an immutable map safe to read from any thread. */ - private fun snapshotCollisions(positions: List, level: ServerLevel): Map { + private fun snapshotCollisions(positions: List, crossings: Set, level: ServerLevel): Map { val snapshot = mutableMapOf() - positions.zipWithNext().forEach { (from, to) -> + positions.zipWithNext().forEachIndexed { segment, (from, to) -> + if (segment in crossings) return@forEachIndexed val dx = to.x - from.x val dy = to.y - from.y val dz = to.z - from.z @@ -145,7 +147,7 @@ object FlightPlanComputer { ): FlightPlan? { val raw = buildRawConstructionCheckpoints(bee, batch, network) ?: return null val checkpoints = if (level != null) insertObstacleWaypoints(raw, level) else raw - return FlightPlan(bee.id, bee.type, DEFAULT_SPEED, checkpoints) + return FlightPlan.of(bee.id, bee.type, DEFAULT_SPEED, checkpoints, level) } /** @@ -155,7 +157,7 @@ object FlightPlanComputer { fun forTransport(bee: ServerBeeData, task: TransportTask, level: ServerLevel? = null): FlightPlan { val raw = buildRawTransportCheckpoints(bee, task) val checkpoints = if (level != null) insertObstacleWaypoints(raw, level) else raw - return FlightPlan(bee.id, bee.type, DEFAULT_SPEED, checkpoints) + return FlightPlan.of(bee.id, bee.type, DEFAULT_SPEED, checkpoints, level) } /** @@ -169,7 +171,7 @@ object FlightPlanComputer { Checkpoint(hiveApproach, EnterHive()), ) val checkpoints = if (level != null) insertObstacleWaypoints(raw, level) else raw - return FlightPlan(bee.id, bee.type, DEFAULT_SPEED, checkpoints) + return FlightPlan.of(bee.id, bee.type, DEFAULT_SPEED, checkpoints, level) } private fun buildRawConstructionCheckpoints(bee: ServerBeeData, batch: TaskBatch, network: BeeNetwork): List? = buildList { @@ -241,15 +243,15 @@ object FlightPlanComputer { /** * Inserts waypoints using a pre-snapshotted collision map. Safe to call from any thread. */ - @Suppress("UNUSED_PARAMETER") private fun insertObstacleWaypointsFromSnapshot( checkpoints: List, collisionSnapshot: Map, - level: ServerLevel, // unused but kept for signature compat + crossings: Set, ): List = buildList { checkpoints.forEachIndexed { index, checkpoint -> add(checkpoint) val next = checkpoints.getOrNull(index + 1) ?: return@forEachIndexed + if (index in crossings) return@forEachIndexed findObstacleWaypointFromSnapshot(checkpoint.pos, next.pos, collisionSnapshot)?.let { add(Checkpoint(it, FlyThrough)) } @@ -288,6 +290,9 @@ object FlightPlanComputer { private const val MAX_FLY_OVER_HEIGHT = 10 + private fun spaceCrossings(checkpoints: List, level: ServerLevel): Set = + SableCompat.flight.crossings(level, checkpoints.map { it.pos }) + /** * Inserts [FlyThrough] waypoints between checkpoints to route around solid blocks. * @@ -297,9 +302,11 @@ object FlightPlanComputer { */ private fun insertObstacleWaypoints(checkpoints: List, level: ServerLevel): List = buildList { + val crossings = spaceCrossings(checkpoints, level) checkpoints.forEachIndexed { index, checkpoint -> add(checkpoint) val next = checkpoints.getOrNull(index + 1) ?: return@forEachIndexed + if (index in crossings) return@forEachIndexed val waypoint = findObstacleWaypoint(checkpoint.pos, next.pos, level) if (waypoint != null) { diff --git a/src/main/kotlin/de/devin/cbbees/content/bee/server/ServerBeeManager.kt b/src/main/kotlin/de/devin/cbbees/content/bee/server/ServerBeeManager.kt index 4d92fe8..dc492c7 100644 --- a/src/main/kotlin/de/devin/cbbees/content/bee/server/ServerBeeManager.kt +++ b/src/main/kotlin/de/devin/cbbees/content/bee/server/ServerBeeManager.kt @@ -174,10 +174,7 @@ object ServerBeeManager { if (bee.currentCheckpointIndex >= plan.checkpoints.size) return val from = plan.checkpoints[prevIndex] - val to = plan.checkpoints[bee.currentCheckpointIndex] - - val travel = FlightPlan.travelTicks(from.pos, to.pos, plan.speed) - bee.nextCheckpointArrivalTick = gameTime + travel + from.clientPauseTicks + bee.nextCheckpointArrivalTick = gameTime + plan.segmentTicks[bee.currentCheckpointIndex] + from.clientPauseTicks } /** @@ -234,11 +231,8 @@ object ServerBeeManager { bee.planStartTick = beeWorld.gameTime bee.currentCheckpointIndex = 0 if (plan.checkpoints.size > 1) { - val travel = FlightPlan.travelTicks( - plan.checkpoints[0].pos, plan.checkpoints[1].pos, plan.speed - ) bee.currentCheckpointIndex = 1 - bee.nextCheckpointArrivalTick = beeWorld.gameTime + travel + bee.nextCheckpointArrivalTick = beeWorld.gameTime + plan.segmentTicks[1] } broadcastFlightPlan(bee, plan, clientStartIndex = 0) } @@ -285,11 +279,8 @@ object ServerBeeManager { bee.planStartTick = beeWorld.gameTime bee.currentCheckpointIndex = 0 if (plan.checkpoints.size > 1) { - val travel = FlightPlan.travelTicks( - plan.checkpoints[0].pos, plan.checkpoints[1].pos, plan.speed - ) bee.currentCheckpointIndex = 1 - bee.nextCheckpointArrivalTick = beeWorld.gameTime + travel + bee.nextCheckpointArrivalTick = beeWorld.gameTime + plan.segmentTicks[1] } broadcastFlightPlan(bee, plan, clientStartIndex = 0) } @@ -315,8 +306,13 @@ object ServerBeeManager { beeId = plan.beeId, type = plan.type, speed = plan.speed, - checkpoints = plan.checkpoints.map { - ClientCheckpoint(it.pos, it.clientPauseTicks, awaitConfirm = it.action is ExecuteBeeAction) + checkpoints = plan.checkpoints.mapIndexed { i, checkpoint -> + ClientCheckpoint( + checkpoint.pos, + checkpoint.clientPauseTicks, + awaitConfirm = checkpoint.action is ExecuteBeeAction, + travelTicks = plan.segmentTicks[i] + ) }, startIndex = clientStartIndex ?: bee.currentCheckpointIndex, elapsedTicks = elapsedTicks, diff --git a/src/main/kotlin/de/devin/cbbees/content/deployer/SchematicDeployerBlockEntity.kt b/src/main/kotlin/de/devin/cbbees/content/deployer/SchematicDeployerBlockEntity.kt index 2d39d7d..3f906fc 100644 --- a/src/main/kotlin/de/devin/cbbees/content/deployer/SchematicDeployerBlockEntity.kt +++ b/src/main/kotlin/de/devin/cbbees/content/deployer/SchematicDeployerBlockEntity.kt @@ -1,5 +1,7 @@ package de.devin.cbbees.content.deployer +import de.devin.cbbees.compat.sable.worldDistanceSqr +import net.minecraft.world.phys.Vec3 import com.simibubi.create.foundation.blockEntity.SmartBlockEntity import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour import de.devin.cbbees.content.bee.debug.BeeDebug @@ -343,7 +345,7 @@ class SchematicDeployerBlockEntity( val snapshot = HiveSnapshot(ClientNetworkInfo("Deployer", 0, 0, 0), listOf(clientJob)) val packet = HiveJobsSyncPacket(blockPos, snapshot) for (player in level.server.playerList.players) { - if (player.level() == level && player.blockPosition().closerThan(blockPos, 128.0)) { + if (player.level() == level && level.worldDistanceSqr(player.position(), Vec3.atCenterOf(blockPos)) < SYNC_RANGE_SQR) { PacketDistributor.sendToPlayer(player, packet) } } @@ -358,7 +360,7 @@ class SchematicDeployerBlockEntity( val snapshot = HiveSnapshot(ClientNetworkInfo("Deployer", 0, 0, 0), emptyList()) val packet = HiveJobsSyncPacket(blockPos, snapshot) for (player in level.server.playerList.players) { - if (player.level() == level && player.blockPosition().closerThan(blockPos, 128.0)) { + if (player.level() == level && level.worldDistanceSqr(player.position(), Vec3.atCenterOf(blockPos)) < SYNC_RANGE_SQR) { PacketDistributor.sendToPlayer(player, packet) } } @@ -412,4 +414,8 @@ class SchematicDeployerBlockEntity( deployResult = DeployResult.entries.getOrElse(ord) { DeployResult.NONE } } } + + companion object { + private const val SYNC_RANGE_SQR = 128.0 * 128.0 + } } diff --git a/src/main/kotlin/de/devin/cbbees/content/domain/GlobalJobPool.kt b/src/main/kotlin/de/devin/cbbees/content/domain/GlobalJobPool.kt index 11d60f0..44a4558 100644 --- a/src/main/kotlin/de/devin/cbbees/content/domain/GlobalJobPool.kt +++ b/src/main/kotlin/de/devin/cbbees/content/domain/GlobalJobPool.kt @@ -1,5 +1,6 @@ package de.devin.cbbees.content.domain +import de.devin.cbbees.compat.sable.worldDistanceSqr import de.devin.cbbees.CreateBuzzyBeez import de.devin.cbbees.config.CBBeesConfig import de.devin.cbbees.content.bee.server.BeeType @@ -270,7 +271,7 @@ object GlobalJobPool : JobPool { if (withPorts.isEmpty()) continue val targetNetwork = withPorts.minByOrNull { network -> - network.hives.minOfOrNull { it.pos.distSqr(batch.targetPosition) } ?: Double.MAX_VALUE + network.hives.minOfOrNull { job.level.worldDistanceSqr(it.pos, batch.targetPosition) } ?: Double.MAX_VALUE } ?: continue if (!hasMaterialsAvailable(batch, targetNetwork)) { noMaterials++; continue } @@ -312,7 +313,7 @@ object GlobalJobPool : JobPool { for (batch in job.batches) { if (!isDispatchable(batch)) continue if (batch.assignedNetworkId != networkId) continue - val dist = batch.targetPosition.distSqr(hivePos) + val dist = beeHive.world.worldDistanceSqr(batch.targetPosition, hivePos) if (dist < bestAssignedDist) { bestAssignedDist = dist bestAssigned = batch @@ -332,7 +333,7 @@ object GlobalJobPool : JobPool { if (!network.isInRange(job.centerPos)) continue val hasDispatchable = job.batches.any { isDispatchable(it) && (it.assignedNetworkId == null || it.assignedNetworkId == networkId) } if (!hasDispatchable) continue - val dist = job.centerPos.distSqr(hivePos) + val dist = beeHive.world.worldDistanceSqr(job.centerPos, hivePos) if (dist < bestJobDist) { bestJobDist = dist bestJob = job diff --git a/src/main/kotlin/de/devin/cbbees/content/domain/TransportDispatcher.kt b/src/main/kotlin/de/devin/cbbees/content/domain/TransportDispatcher.kt index 51e9c3f..422414e 100644 --- a/src/main/kotlin/de/devin/cbbees/content/domain/TransportDispatcher.kt +++ b/src/main/kotlin/de/devin/cbbees/content/domain/TransportDispatcher.kt @@ -1,5 +1,6 @@ package de.devin.cbbees.content.domain +import de.devin.cbbees.compat.sable.worldDistanceSqr import de.devin.cbbees.content.bee.BeeSeparation import de.devin.cbbees.content.bee.MechanicalBumbleBeeEntity import de.devin.cbbees.content.bee.server.BeeType @@ -101,7 +102,7 @@ object TransportDispatcher : SavedData() { // Use minByOrNull instead of sortedBy.firstOrNull to avoid full sort val hive = hivesWithBumbles .filter { it.hasBeeOfType(BeeType.TRANSPORT) && it.canSpawnBee() } - .minByOrNull { it.pos.distSqr(provider.pos) } + .minByOrNull { it.world.worldDistanceSqr(it.pos, provider.pos) } ?: break // no more bumble bees available in any hive val beeItem = hive.consumeBeeOfType(BeeType.TRANSPORT) diff --git a/src/main/kotlin/de/devin/cbbees/content/domain/beehive/BeeHive.kt b/src/main/kotlin/de/devin/cbbees/content/domain/beehive/BeeHive.kt index fc9415e..32ae85f 100644 --- a/src/main/kotlin/de/devin/cbbees/content/domain/beehive/BeeHive.kt +++ b/src/main/kotlin/de/devin/cbbees/content/domain/beehive/BeeHive.kt @@ -1,5 +1,7 @@ package de.devin.cbbees.content.domain.beehive +import kotlin.math.abs +import de.devin.cbbees.compat.sable.toWorld import de.devin.cbbees.config.CBBeesConfig import de.devin.cbbees.content.bee.MechanicalBeeEntity import de.devin.cbbees.content.bee.server.BeeType @@ -78,10 +80,10 @@ interface BeeHive : INetworkComponent { * Checks if a position is within the work range of this source. */ fun isInRange(pos: BlockPos): Boolean { - val dx = Math.abs(pos.x - this.pos.x) - val dz = Math.abs(pos.z - this.pos.z) + val target = world.toWorld(pos) + val origin = world.toWorld(this.pos) val range = getWorkRange().toInt() - return dx <= range && dz <= range + return abs(target.x - origin.x) <= range && abs(target.z - origin.z) <= range } /** diff --git a/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetwork.kt b/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetwork.kt index 338aecf..45845f1 100644 --- a/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetwork.kt +++ b/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetwork.kt @@ -1,5 +1,6 @@ package de.devin.cbbees.content.domain.network +import de.devin.cbbees.compat.sable.worldDistanceSqr import de.devin.cbbees.CreateBuzzyBeez import de.devin.cbbees.content.domain.beehive.BeeHive import de.devin.cbbees.content.domain.beehive.PortableBeeHive @@ -198,7 +199,7 @@ class BeeNetwork( val candidates = hives.filter { topology.isOperationalRange(it, batch.targetPosition) && it.getAvailableBeeCount() > 0 - }.sortedBy { it.pos.distSqr(batch.targetPosition) } + }.sortedBy { it.world.worldDistanceSqr(it.pos, batch.targetPosition) } for (hive in candidates) { if (hive.acceptBatch(batch)) { diff --git a/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetworkManager.kt b/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetworkManager.kt index 2b2fd86..2605f12 100644 --- a/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetworkManager.kt +++ b/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetworkManager.kt @@ -195,23 +195,30 @@ object ServerBeeNetworkManager { return } + splitIfDisconnected(network) + rebuildIndexes() + } + + fun mergeNetworks(target: BeeNetwork, other: BeeNetwork) { + target.merge(other) + networks.remove(other) + } + + fun splitIfDisconnected(network: BeeNetwork): Boolean { val splitResults = network.split() if (splitResults.isEmpty()) { - val remaining = network.components.toList() - remaining.forEach { + network.components.toList().forEach { it.networkId = UUID.randomUUID() it.sync() } networks.remove(network) - } else if (splitResults.size > 1) { - splitResults.forEach { result -> - if (!networks.contains(result)) { - networks.add(result) - } - } + return true } - - rebuildIndexes() + if (splitResults.size > 1) { + splitResults.filterNot(networks::contains).forEach(networks::add) + return true + } + return false } fun registerWorker(worker: BeeHive) = registerComponent(worker) diff --git a/src/main/kotlin/de/devin/cbbees/content/domain/network/topology/DefaultAnchorTopology.kt b/src/main/kotlin/de/devin/cbbees/content/domain/network/topology/DefaultAnchorTopology.kt index 700c72e..98e8b9f 100644 --- a/src/main/kotlin/de/devin/cbbees/content/domain/network/topology/DefaultAnchorTopology.kt +++ b/src/main/kotlin/de/devin/cbbees/content/domain/network/topology/DefaultAnchorTopology.kt @@ -1,5 +1,6 @@ package de.devin.cbbees.content.domain.network.topology +import de.devin.cbbees.compat.sable.toWorld import de.devin.cbbees.content.domain.network.INetworkComponent import net.minecraft.core.BlockPos import kotlin.math.abs @@ -15,8 +16,10 @@ object DefaultAnchorTopology : NetworkTopology { override fun isAnchor(component: INetworkComponent): Boolean = component.isAnchor() override fun canConnectAnchors(a: INetworkComponent, b: INetworkComponent): Boolean { - val dx = abs(a.pos.x - b.pos.x) - val dz = abs(a.pos.z - b.pos.z) + val posA = a.world.toWorld(a.pos) + val posB = b.world.toWorld(b.pos) + val dx = abs(posA.x - posB.x) + val dz = abs(posA.z - posB.z) val combined = a.getNetworkingRange() + b.getNetworkingRange() return dx <= combined && dz <= combined } diff --git a/src/main/kotlin/de/devin/cbbees/content/schematics/client/ConstructionRenderer.kt b/src/main/kotlin/de/devin/cbbees/content/schematics/client/ConstructionRenderer.kt index 93a33b7..66386b1 100644 --- a/src/main/kotlin/de/devin/cbbees/content/schematics/client/ConstructionRenderer.kt +++ b/src/main/kotlin/de/devin/cbbees/content/schematics/client/ConstructionRenderer.kt @@ -8,6 +8,7 @@ import com.simibubi.create.content.schematics.client.SchematicRenderer import net.minecraft.client.renderer.RenderType.chunkBufferLayers import com.simibubi.create.foundation.blockEntity.IMultiBlockEntityContainer import com.simibubi.create.foundation.blockEntity.SmartBlockEntity +import de.devin.cbbees.compat.sable.SableCompat import de.devin.cbbees.CreateBuzzyBeez import de.devin.cbbees.config.CBBeesClientConfig import de.devin.cbbees.content.beehive.client.ClientJobCache @@ -142,19 +143,26 @@ object ConstructionRenderer { val opacity = CBBeesClientConfig.ghostBlockOpacity.get().toFloat() val transparentBuffer = TransparentBuffer(superBuffer, opacity) + val pt = AnimationTickHolder.getPartialTicks() + profiler.push("renderGhosts") for ((_, jobRenderer) in rendererCache) { - poseStack.pushPose() - poseStack.translate(-camera.x, -camera.y, -camera.z) - jobRenderer.renderer.render(poseStack, transparentBuffer) - poseStack.popPose() + val anchor = jobRenderer.anchor + SableCompat.render.draw(poseStack, anchor, camera, pt) { cam -> + poseStack.pushPose() + poseStack.translate(anchor.x - cam.x, anchor.y - cam.y, anchor.z - cam.z) + jobRenderer.renderer.render(poseStack, transparentBuffer) + poseStack.popPose() + } } profiler.pop() profiler.push("renderOutlines") - val pt = AnimationTickHolder.getPartialTicks() - for ((_, outline) in outlineCache) { - outline.render(poseStack, superBuffer, camera, pt) + for ((jobId, outline) in outlineCache) { + val anchor = jobAnchor(jobId) ?: continue + SableCompat.render.draw(poseStack, anchor, camera, pt) { cam -> + outline.render(poseStack, superBuffer, cam.subtract(Vec3.atLowerCornerOf(anchor)), pt) + } } profiler.pop() @@ -197,7 +205,8 @@ object ConstructionRenderer { (positions.maxOf { it.z } + 1).toDouble() ) outlineBoundsCache[job.jobId] = bounds - val outline = AABBOutline(bounds) + val anchor = renderer?.anchor ?: BlockPos.containing(bounds.minX, bounds.minY, bounds.minZ) + val outline = AABBOutline(bounds.move(Vec3.atLowerCornerOf(anchor).scale(-1.0))) val color = if (job.reason != null) STUCK_COLOR else NORMAL_COLOR outline.params .colored(color) @@ -277,7 +286,7 @@ object ConstructionRenderer { removePlacedBlocks(schematicLevel, clientLevel) - return JobRenderer(GhostSchematicRenderer(schematicLevel), schematicLevel, anchor) + return JobRenderer(GhostSchematicRenderer(schematicLevel, anchor), schematicLevel, anchor) } catch (e: Exception) { CreateBuzzyBeez.LOGGER.error("Failed to load schematic for rendering: $schematicFile", e) return buildFallbackRenderer(job, clientLevel) @@ -301,7 +310,8 @@ object ConstructionRenderer { val ghostLevel = GhostBlockLevel(clientLevel) ghostLevel.populate(allGhosts) - return JobRenderer(GhostSchematicRenderer(ghostLevel), ghostLevel, BlockPos.ZERO) + val origin = BlockPos(allGhosts.keys.minOf { it.x }, allGhosts.keys.minOf { it.y }, allGhosts.keys.minOf { it.z }) + return JobRenderer(GhostSchematicRenderer(ghostLevel, origin), ghostLevel, origin) } /** @@ -318,10 +328,14 @@ object ConstructionRenderer { var bestDist = maxRange var bestJob: UUID? = null + val pt = AnimationTickHolder.getPartialTicks() for ((jobId, bounds) in outlineBoundsCache) { - val hit = bounds.clip(start, start.add(dir.scale(maxRange))) + val anchor = jobAnchor(jobId) ?: continue + val localStart = SableCompat.render.toLocal(anchor, start, pt) + val localEnd = SableCompat.render.toLocal(anchor, start.add(dir.scale(maxRange)), pt) + val hit = bounds.clip(localStart, localEnd) if (hit.isPresent) { - val dist = hit.get().distanceTo(start) + val dist = hit.get().distanceTo(localStart) if (dist < bestDist) { bestDist = dist bestJob = jobId @@ -331,6 +345,9 @@ object ConstructionRenderer { return bestJob?.let { it to bestDist } } + private fun jobAnchor(jobId: UUID): BlockPos? = + rendererCache[jobId]?.anchor ?: outlineBoundsCache[jobId]?.let { BlockPos.containing(it.minX, it.minY, it.minZ) } + /** * Returns the cached [ClientJobInfo] for a given job ID, or null. */ diff --git a/src/main/kotlin/de/devin/cbbees/content/schematics/client/GhostSchematicRenderer.kt b/src/main/kotlin/de/devin/cbbees/content/schematics/client/GhostSchematicRenderer.kt index dd79ffb..86d8536 100644 --- a/src/main/kotlin/de/devin/cbbees/content/schematics/client/GhostSchematicRenderer.kt +++ b/src/main/kotlin/de/devin/cbbees/content/schematics/client/GhostSchematicRenderer.kt @@ -33,7 +33,10 @@ import java.util.BitSet * Geometry can be pre-built on a background thread via [prebuildGeometry], * eliminating the frame stutter that would otherwise occur on first render. */ -class GhostSchematicRenderer(world: SchematicLevel) : SchematicRenderer(world) { +class GhostSchematicRenderer( + world: SchematicLevel, + private val origin: BlockPos = BlockPos.ZERO +) : SchematicRenderer(world) { private val anchor: BlockPos = world.anchor @@ -108,11 +111,14 @@ class GhostSchematicRenderer(world: SchematicLevel) : SchematicRenderer(world) { val shouldRender = BitSet(capped.size) shouldRender.set(0, capped.size) val errored = BitSet(capped.size) + ms.pushPose() + ms.translate(-origin.x.toDouble(), -origin.y.toDouble(), -origin.z.toDouble()) BlockEntityRenderHelper.renderBlockEntities( capped, shouldRender, errored, null, mc.level, ms, null, buffers, AnimationTickHolder.getPartialTicks() ) + ms.popPose() } } @@ -146,7 +152,11 @@ class GhostSchematicRenderer(world: SchematicLevel) : SchematicRenderer(world) { random.setSeed(seed) if (model.getRenderTypes(state, random, finalModelData).contains(layer)) { poseStack.pushPose() - poseStack.translate(localPos.x.toDouble(), localPos.y.toDouble(), localPos.z.toDouble()) + poseStack.translate( + (localPos.x - origin.x).toDouble(), + (localPos.y - origin.y).toDouble(), + (localPos.z - origin.z).toDouble() + ) renderer.tesselateBlock( renderWorld, model, state, pos, poseStack, sbbBuilder, true, diff --git a/src/main/kotlin/de/devin/cbbees/content/schematics/client/SchematicHoverPreview.kt b/src/main/kotlin/de/devin/cbbees/content/schematics/client/SchematicHoverPreview.kt index db93c80..540b72d 100644 --- a/src/main/kotlin/de/devin/cbbees/content/schematics/client/SchematicHoverPreview.kt +++ b/src/main/kotlin/de/devin/cbbees/content/schematics/client/SchematicHoverPreview.kt @@ -3,6 +3,7 @@ package de.devin.cbbees.content.schematics.client import com.mojang.blaze3d.systems.RenderSystem import com.mojang.blaze3d.vertex.VertexConsumer import com.simibubi.create.AllDataComponents +import de.devin.cbbees.compat.sable.SableCompat import de.devin.cbbees.config.CBBeesClientConfig import com.simibubi.create.content.schematics.SchematicItem import com.simibubi.create.foundation.utility.RaycastHelper @@ -215,31 +216,35 @@ object SchematicHoverPreview { val zO = schematicSize.z / 2.0 val rot = -(currentRotation.ordinal * 90.0).toFloat() + val partialTick = event.partialTick.getGameTimeDeltaPartialTick(false) + // Render ghost blocks only if renderer is ready (built asynchronously) val renderer = getActiveRenderer() if (renderer != null) { val opacity = CBBeesClientConfig.ghostBlockOpacity.get().toFloat() val transparentBuffer = TransparentBuffer(superBuffer, opacity) - poseStack.pushPose() + SableCompat.render.draw(poseStack, anchor, camera, partialTick) { cam -> + poseStack.pushPose() - TransformStack.of(poseStack) - .translate(Vec3.atLowerCornerOf(target).subtract(camera)) - poseStack.translate(xO, 0.0, zO) - TransformStack.of(poseStack).rotateYDegrees(rot) - poseStack.translate(-xO, 0.0, -zO) + TransformStack.of(poseStack) + .translate(Vec3.atLowerCornerOf(target).subtract(cam)) + poseStack.translate(xO, 0.0, zO) + TransformStack.of(poseStack).rotateYDegrees(rot) + poseStack.translate(-xO, 0.0, -zO) - renderer.render(poseStack, transparentBuffer) - poseStack.popPose() + renderer.render(poseStack, transparentBuffer) + poseStack.popPose() + } } // Always render AABB outline (available immediately, lightweight) val aabbOutline = outline if (aabbOutline != null) { - val aabb = computeTransformedAABB(target) - aabbOutline.setBounds(aabb) - poseStack.pushPose() - aabbOutline.render(poseStack, superBuffer, camera, event.partialTick.getGameTimeDeltaPartialTick(false)) - poseStack.popPose() + val origin = Vec3.atLowerCornerOf(anchor) + aabbOutline.setBounds(computeTransformedAABB(target).move(origin.scale(-1.0))) + SableCompat.render.draw(poseStack, anchor, camera, partialTick) { cam -> + aabbOutline.render(poseStack, superBuffer, cam.subtract(origin), partialTick) + } } superBuffer.draw() diff --git a/src/main/kotlin/de/devin/cbbees/gametest/flight/FlightPlanTests.kt b/src/main/kotlin/de/devin/cbbees/gametest/flight/FlightPlanTests.kt index 3ab7b0a..7250b58 100644 --- a/src/main/kotlin/de/devin/cbbees/gametest/flight/FlightPlanTests.kt +++ b/src/main/kotlin/de/devin/cbbees/gametest/flight/FlightPlanTests.kt @@ -56,10 +56,8 @@ object FlightPlanTests { // ── estimatedDurationTicks ── - private fun plan(vararg checkpoints: Checkpoint) = FlightPlan( - beeId = UUID.randomUUID(), type = BeeType.CONSTRUCTION, speed = 1.0f, - checkpoints = checkpoints.toList(), - ) + private fun plan(vararg checkpoints: Checkpoint) = + FlightPlan.of(UUID.randomUUID(), BeeType.CONSTRUCTION, 1.0f, checkpoints.toList(), null) @GameTest(template = TEMPLATE, timeoutTicks = 20) @JvmStatic @@ -116,13 +114,14 @@ object FlightPlanTests { @GameTest(template = TEMPLATE, timeoutTicks = 20) @JvmStatic fun arrivalTicks_matchesDuration(helper: GameTestHelper) = helper.unitTest { - val p = FlightPlan( - beeId = UUID.randomUUID(), type = BeeType.CONSTRUCTION, speed = 0.5f, - checkpoints = listOf( + val p = FlightPlan.of( + UUID.randomUUID(), BeeType.CONSTRUCTION, 0.5f, + listOf( Checkpoint(BlockPos(0, 0, 0), FlyThrough, clientPauseTicks = 2), Checkpoint(BlockPos(5, 0, 0), FlyThrough, clientPauseTicks = 4), Checkpoint(BlockPos(5, 10, 0), FlyThrough), ), + null ) assertEquals(p.estimatedDurationTicks, p.computeArrivalTicks().last(), "last arrival = duration") } diff --git a/src/main/kotlin/de/devin/cbbees/network/BeeFlightPlanPacket.kt b/src/main/kotlin/de/devin/cbbees/network/BeeFlightPlanPacket.kt index 83240e4..611ef19 100644 --- a/src/main/kotlin/de/devin/cbbees/network/BeeFlightPlanPacket.kt +++ b/src/main/kotlin/de/devin/cbbees/network/BeeFlightPlanPacket.kt @@ -45,6 +45,7 @@ class BeeFlightPlanPacket( pos = buf.readBlockPos(), pauseTicks = buf.readVarInt(), awaitConfirm = buf.readBoolean(), + travelTicks = buf.readVarLong(), ) }, startIndex = buf.readVarInt(), @@ -60,6 +61,7 @@ class BeeFlightPlanPacket( buf.writeBlockPos(cp.pos) buf.writeVarInt(cp.pauseTicks) buf.writeBoolean(cp.awaitConfirm) + buf.writeVarLong(cp.travelTicks) } buf.writeVarInt(packet.startIndex) buf.writeVarLong(packet.elapsedTicks) diff --git a/src/main/kotlin/de/devin/cbbees/network/DeployerSettingsPacket.kt b/src/main/kotlin/de/devin/cbbees/network/DeployerSettingsPacket.kt index 935c649..d20c758 100644 --- a/src/main/kotlin/de/devin/cbbees/network/DeployerSettingsPacket.kt +++ b/src/main/kotlin/de/devin/cbbees/network/DeployerSettingsPacket.kt @@ -1,5 +1,7 @@ package de.devin.cbbees.network +import net.minecraft.world.phys.Vec3 +import de.devin.cbbees.compat.sable.worldDistanceSqr import de.devin.cbbees.CreateBuzzyBeez import de.devin.cbbees.content.deployer.DeployMode import de.devin.cbbees.content.deployer.SchematicDeployerBlockEntity @@ -53,7 +55,7 @@ class DeployerSettingsPacket( val player = context.player() as? ServerPlayer ?: return@enqueueWork val level = player.serverLevel() - if (player.blockPosition().distSqr(payload.deployerPos) > 64.0) return@enqueueWork + if (level.worldDistanceSqr(player.position(), Vec3.atCenterOf(payload.deployerPos)) > 64.0) return@enqueueWork val be = level.getBlockEntity(payload.deployerPos) as? SchematicDeployerBlockEntity ?: return@enqueueWork diff --git a/src/main/templates/META-INF/neoforge.mods.toml b/src/main/templates/META-INF/neoforge.mods.toml index 53267ad..ca4a822 100644 --- a/src/main/templates/META-INF/neoforge.mods.toml +++ b/src/main/templates/META-INF/neoforge.mods.toml @@ -75,10 +75,17 @@ side = "BOTH" [[dependencies."${mod_id}"]] modId="create" type="required" -versionRange="[6.0.9,6.1.0)" +versionRange="[6.0.10,6.1.0)" ordering="NONE" side="BOTH" +[[dependencies."${mod_id}"]] +modId="sable-companion" +type="optional" +versionRange="[1.6.0,)" +ordering="AFTER" +side="BOTH" + # Features are specific properties of the game environment, that you may want to declare you require. This example declares # that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't # stop your mod loading on the server for example. From 34d7bb8effb4162b221cc4f8a9808030d1f44e5a Mon Sep 17 00:00:00 2001 From: devin Date: Fri, 16 Oct 2026 22:36:05 +0200 Subject: [PATCH 2/2] Sable compat v2 --- .../compat/sable/SubLevelNetworkTracker.kt | 12 ++++++- .../beehive/MechanicalBeehiveBlockEntity.kt | 13 +++----- .../beehive/MechanicalBeehiveScreen.kt | 2 +- .../beehive/client/BeehiveRangeHandler.kt | 5 ++- .../content/domain/beehive/PortableBeeHive.kt | 2 +- .../content/domain/network/BeeNetwork.kt | 8 +++-- .../domain/network/BeeNetworkManager.kt | 33 +++++++++++++------ .../domain/network/INetworkComponent.kt | 21 ++++++++---- .../content/domain/network/NetworkTooltip.kt | 17 ++++++++++ .../network/client/NetworkHighlightHandler.kt | 3 +- .../ports/LogisticsPortBlockEntity.kt | 17 +++------- .../transport/TransportPortBlockEntity.kt | 15 +++------ .../gametest/network/NetworkLinkingTests.kt | 4 +-- .../resources/assets/cbbees/lang/en_us.json | 1 + 14 files changed, 92 insertions(+), 61 deletions(-) create mode 100644 src/main/kotlin/de/devin/cbbees/content/domain/network/NetworkTooltip.kt diff --git a/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelNetworkTracker.kt b/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelNetworkTracker.kt index 044cb28..24f33c0 100644 --- a/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelNetworkTracker.kt +++ b/src/main/kotlin/de/devin/cbbees/compat/sable/SubLevelNetworkTracker.kt @@ -5,6 +5,7 @@ import de.devin.cbbees.content.domain.beehive.PortableBeeHive import de.devin.cbbees.content.domain.network.BeeNetwork import de.devin.cbbees.content.domain.network.ServerBeeNetworkManager import de.devin.cbbees.util.ServerSide +import net.minecraft.core.BlockPos import net.neoforged.bus.api.SubscribeEvent import net.neoforged.neoforge.event.tick.ServerTickEvent @@ -19,7 +20,8 @@ object SubLevelNetworkTracker { if (event.server.tickCount % INTERVAL_TICKS != 0) return val split = splitDisconnected() val merged = mergeReconnected() - if (split || merged) ServerBeeNetworkManager.rebuildIndexes() + val adopted = adoptOrphans() + if (split || merged || adopted) ServerBeeNetworkManager.rebuildIndexes() } private fun splitDisconnected(): Boolean = @@ -45,6 +47,14 @@ object SubLevelNetworkTracker { return merged } + private fun adoptOrphans(): Boolean = + movingNetworks().flatMap { network -> + network.components.filter { it.isAnchor() }.map { anchor -> + val center = BlockPos.containing(anchor.world.toWorld(anchor.pos)) + ServerBeeNetworkManager.adoptOrphans(network, anchor.world, center, anchor.getNetworkingRange()) + } + }.any { it } + private fun movingNetworks() = ServerBeeNetworkManager.getNetworks().filter { network -> !network.isPortable() && network.components.any { it.world.subLevelId(it.pos) != null } } diff --git a/src/main/kotlin/de/devin/cbbees/content/beehive/MechanicalBeehiveBlockEntity.kt b/src/main/kotlin/de/devin/cbbees/content/beehive/MechanicalBeehiveBlockEntity.kt index 378da4f..ab507b3 100644 --- a/src/main/kotlin/de/devin/cbbees/content/beehive/MechanicalBeehiveBlockEntity.kt +++ b/src/main/kotlin/de/devin/cbbees/content/beehive/MechanicalBeehiveBlockEntity.kt @@ -2,6 +2,7 @@ package de.devin.cbbees.content.beehive import com.simibubi.create.api.equipment.goggles.IHaveGoggleInformation import com.simibubi.create.content.kinetics.base.KineticBlockEntity +import de.devin.cbbees.content.domain.network.addNetworkTooltip import de.devin.cbbees.content.bee.* import de.devin.cbbees.content.bee.server.BeeType import de.devin.cbbees.content.bee.server.ServerBeeManager @@ -187,7 +188,7 @@ class MechanicalBeehiveBlockEntity(type: BlockEntityType<*>, pos: BlockPos, stat } - override var networkId: UUID = UUID.randomUUID() + override var networkId: UUID? = UUID.randomUUID() set(value) { if (field == value) return val old = field @@ -217,7 +218,7 @@ class MechanicalBeehiveBlockEntity(type: BlockEntityType<*>, pos: BlockPos, stat override fun write(tag: CompoundTag, registries: HolderLookup.Provider, clientPacket: Boolean) { super.write(tag, registries, clientPacket) tag.putUUID("HomeId", homeId) - tag.putUUID("NetworkId", networkId) + networkId?.let { tag.putUUID("NetworkId", it) } val activeBeesList = ListTag() activeBeesByJob.forEach { (jobId, bees) -> @@ -384,13 +385,7 @@ class MechanicalBeehiveBlockEntity(type: BlockEntityType<*>, pos: BlockPos, stat Lang.builder("cbbees").translate("gui.goggles.beehive_stats") .forGoggles(tooltip) - val net = network() - net.let { n -> - Lang.builder("cbbees").translate("gui.goggles.beehive.network") - .style(ChatFormatting.GRAY) - .add(Lang.builder("cbbees").text(n.name).style(ChatFormatting.GOLD)) - .forGoggles(tooltip, 1) - } + addNetworkTooltip(tooltip, 1) Lang.builder("cbbees").translate("gui.goggles.beehive.flying") .style(ChatFormatting.GRAY) diff --git a/src/main/kotlin/de/devin/cbbees/content/beehive/MechanicalBeehiveScreen.kt b/src/main/kotlin/de/devin/cbbees/content/beehive/MechanicalBeehiveScreen.kt index e6b38a8..beb7fff 100644 --- a/src/main/kotlin/de/devin/cbbees/content/beehive/MechanicalBeehiveScreen.kt +++ b/src/main/kotlin/de/devin/cbbees/content/beehive/MechanicalBeehiveScreen.kt @@ -118,7 +118,7 @@ class MechanicalBeehiveScreen( }) hiliteBtn.withCallback(Runnable { val beeIds = job.batches.flatMap { it.assignedBeeIds }.distinct() - JobHighlightHandler.toggle(menu.content.networkId, job.jobId, beeIds, true) + menu.content.networkId?.let { JobHighlightHandler.toggle(it, job.jobId, beeIds, true) } }) addRenderableWidget(cancelBtn) addRenderableWidget(hiliteBtn) diff --git a/src/main/kotlin/de/devin/cbbees/content/beehive/client/BeehiveRangeHandler.kt b/src/main/kotlin/de/devin/cbbees/content/beehive/client/BeehiveRangeHandler.kt index ec61976..2903331 100644 --- a/src/main/kotlin/de/devin/cbbees/content/beehive/client/BeehiveRangeHandler.kt +++ b/src/main/kotlin/de/devin/cbbees/content/beehive/client/BeehiveRangeHandler.kt @@ -80,10 +80,9 @@ object BeehiveRangeHandler { private fun renderRange(be: MechanicalBeehiveBlockEntity, slotKeys: MutableSet) { - val networkId = be.networkId - val network = ClientBeeNetworkManager.getNetwork(networkId) + val network = be.networkId?.let(ClientBeeNetworkManager::getNetwork) val y = be.blockPos.y.toDouble() - if (network.hives.isEmpty()) { + if (network == null || network.hives.isEmpty()) { renderSingleHiveRange(be, slotKeys) } else { renderNetworkRange(network, y, slotKeys) diff --git a/src/main/kotlin/de/devin/cbbees/content/domain/beehive/PortableBeeHive.kt b/src/main/kotlin/de/devin/cbbees/content/domain/beehive/PortableBeeHive.kt index 60c1b37..24b9f48 100644 --- a/src/main/kotlin/de/devin/cbbees/content/domain/beehive/PortableBeeHive.kt +++ b/src/main/kotlin/de/devin/cbbees/content/domain/beehive/PortableBeeHive.kt @@ -132,7 +132,7 @@ class PortableBeeHive(val player: Player) : BeeHive, LogisticsPort { override val id: UUID get() = player.uuid override val world: Level get() = player.level() override val pos: BlockPos get() = player.blockPosition() - override var networkId: UUID = UUID.randomUUID() + override var networkId: UUID? = UUID.randomUUID() set(value) { if (field == value) return val old = field diff --git a/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetwork.kt b/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetwork.kt index 45845f1..5263325 100644 --- a/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetwork.kt +++ b/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetwork.kt @@ -82,7 +82,10 @@ class BeeNetwork( val inRange = _components.any { other -> topology.isAnchor(other) && topology.isLogisticsRange(other, comp.pos) } - if (!inRange) return@removeAll true + if (!inRange) { + comp.orphan() + return@removeAll true + } } false } @@ -293,8 +296,7 @@ class BeeNetwork( for (c in nonAnchors) { if (!group.any { topology.isOperationalRange(it, c.pos) }) { components.remove(c) - c.networkId = UUID.randomUUID() - c.sync() + c.orphan() } } return listOf(this) diff --git a/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetworkManager.kt b/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetworkManager.kt index 2605f12..a5ee434 100644 --- a/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetworkManager.kt +++ b/src/main/kotlin/de/devin/cbbees/content/domain/network/BeeNetworkManager.kt @@ -92,7 +92,7 @@ object ServerBeeNetworkManager { // always pass the spatial range check — they get picked up by // scanAndJoinNearbyComponents when a beehive registers. if (component.isAnchor()) { - val idNetwork = networkById[component.networkId] + val idNetwork = component.networkId?.let { networkById[it] } if (idNetwork != null && !nearbyNetworks.contains(idNetwork)) { if (idNetwork.level == null || idNetwork.level == component.world) { nearbyNetworks.add(idNetwork) @@ -104,10 +104,7 @@ object ServerBeeNetworkManager { // Non-anchor components (logistics ports) cannot create their own network. // They must join an existing network that has an anchor (mechanical beehive). if (nearbyNetworks.isEmpty() && !component.isAnchor()) { - // Assign a unique networkId so orphaned ports with stale saved IDs - // don't accidentally appear grouped on the client. - component.networkId = UUID.randomUUID() - component.sync() + component.orphan() CreateBuzzyBeez.LOGGER.info("[NET] REJECTED: no network with anchor for ${component.javaClass.simpleName} at ${component.pos}") return } @@ -115,7 +112,7 @@ object ServerBeeNetworkManager { val targetNetwork: BeeNetwork if (nearbyNetworks.isEmpty()) { - targetNetwork = BeeNetwork(component.networkId, topology) + targetNetwork = BeeNetwork(component.networkId ?: UUID.randomUUID(), topology) networks.add(targetNetwork) CreateBuzzyBeez.LOGGER.info("[NET] CREATED new network ${targetNetwork.id}") } else { @@ -199,6 +196,24 @@ object ServerBeeNetworkManager { rebuildIndexes() } + fun adoptOrphans(network: BeeNetwork, level: Level, center: BlockPos, range: Double): Boolean { + val r = range.toInt() + var adopted = false + for (cx in ((center.x - r) shr 4)..((center.x + r) shr 4)) { + for (cz in ((center.z - r) shr 4)..((center.z + r) shr 4)) { + val chunk = level.chunkSource.getChunk(cx, cz, false) ?: continue + chunk.blockEntities.values + .filterIsInstance() + .filter { it.isOrphaned && !it.isAnchor() && network.isInLogisticsRange(it.pos) } + .forEach { + network.addComponent(it) + adopted = true + } + } + } + return adopted + } + fun mergeNetworks(target: BeeNetwork, other: BeeNetwork) { target.merge(other) networks.remove(other) @@ -207,10 +222,7 @@ object ServerBeeNetworkManager { fun splitIfDisconnected(network: BeeNetwork): Boolean { val splitResults = network.split() if (splitResults.isEmpty()) { - network.components.toList().forEach { - it.networkId = UUID.randomUUID() - it.sync() - } + network.components.toList().forEach { it.orphan() } networks.remove(network) return true } @@ -408,6 +420,7 @@ object ClientBeeNetworkManager { if (correctNetworkId == null) { val net = networks.find { it.components.contains(component) } net?.removeComponent(component) + if (!component.isAnchor()) component.networkId = null } else if (correctNetworkId != component.networkId) { val oldNet = networks.find { it.components.contains(component) } oldNet?.removeComponent(component) diff --git a/src/main/kotlin/de/devin/cbbees/content/domain/network/INetworkComponent.kt b/src/main/kotlin/de/devin/cbbees/content/domain/network/INetworkComponent.kt index 02eca4d..8fe0368 100644 --- a/src/main/kotlin/de/devin/cbbees/content/domain/network/INetworkComponent.kt +++ b/src/main/kotlin/de/devin/cbbees/content/domain/network/INetworkComponent.kt @@ -9,28 +9,35 @@ interface INetworkComponent { val id: UUID val world: Level val pos: BlockPos - var networkId: UUID + var networkId: UUID? + + val isOrphaned: Boolean get() = networkId == null + + fun orphan() { + networkId = null + sync() + } fun network(): BeeNetwork { return if (world.isClientSide) { - ClientBeeNetworkManager.getNetwork(networkId) + networkId?.let(ClientBeeNetworkManager::getNetwork) ?: BeeNetwork() } else { ServerBeeNetworkManager.getNetworkFor(this) ?: run { ServerBeeNetworkManager.registerComponent(this) ServerBeeNetworkManager.getNetworkFor(this) - } ?: BeeNetwork(networkId) + } ?: networkId?.let(::BeeNetwork) ?: BeeNetwork() } } /** * Helper method to handle re-grouping on the client when the networkId changes. */ - fun onNetworkIdChanged(old: UUID, new: UUID) { + fun onNetworkIdChanged(old: UUID?, new: UUID?) { val level = (this as? SmartBlockEntity)?.level ?: return if (level.isClientSide) { - ClientBeeNetworkManager.getNetwork(old).removeComponent(this) - ClientBeeNetworkManager.getNetwork(new).addComponentClient(this) + old?.let { ClientBeeNetworkManager.getNetwork(it).removeComponent(this) } + new?.let { ClientBeeNetworkManager.getNetwork(it).addComponentClient(this) } } } @@ -53,7 +60,7 @@ interface INetworkComponent { if (!level.isClientSide) { ServerBeeNetworkManager.registerComponent(this) } else { - ClientBeeNetworkManager.getNetwork(networkId).addComponentClient(this) + networkId?.let { ClientBeeNetworkManager.getNetwork(it).addComponentClient(this) } } } diff --git a/src/main/kotlin/de/devin/cbbees/content/domain/network/NetworkTooltip.kt b/src/main/kotlin/de/devin/cbbees/content/domain/network/NetworkTooltip.kt new file mode 100644 index 0000000..673adcc --- /dev/null +++ b/src/main/kotlin/de/devin/cbbees/content/domain/network/NetworkTooltip.kt @@ -0,0 +1,17 @@ +package de.devin.cbbees.content.domain.network + +import net.createmod.catnip.lang.Lang +import net.minecraft.ChatFormatting +import net.minecraft.network.chat.Component + +fun INetworkComponent.addNetworkTooltip(tooltip: MutableList, indent: Int = 0) { + val value = if (isOrphaned) { + Lang.builder("cbbees").translate("gui.goggles.network.none").style(ChatFormatting.RED) + } else { + Lang.builder("cbbees").text(network().name).style(ChatFormatting.GOLD) + } + Lang.builder("cbbees").translate("gui.goggles.beehive.network") + .style(ChatFormatting.GRAY) + .add(value) + .forGoggles(tooltip, indent) +} diff --git a/src/main/kotlin/de/devin/cbbees/content/domain/network/client/NetworkHighlightHandler.kt b/src/main/kotlin/de/devin/cbbees/content/domain/network/client/NetworkHighlightHandler.kt index 2ca79aa..4b047f6 100644 --- a/src/main/kotlin/de/devin/cbbees/content/domain/network/client/NetworkHighlightHandler.kt +++ b/src/main/kotlin/de/devin/cbbees/content/domain/network/client/NetworkHighlightHandler.kt @@ -43,8 +43,7 @@ object NetworkHighlightHandler { } private fun renderNetworkBlink(component: INetworkComponent) { - val networkId = component.networkId - val network = ClientBeeNetworkManager.getNetwork(networkId) + val network = component.networkId?.let(ClientBeeNetworkManager::getNetwork) ?: return if (network.components.isEmpty()) return diff --git a/src/main/kotlin/de/devin/cbbees/content/logistics/ports/LogisticsPortBlockEntity.kt b/src/main/kotlin/de/devin/cbbees/content/logistics/ports/LogisticsPortBlockEntity.kt index 5424a18..c38c448 100644 --- a/src/main/kotlin/de/devin/cbbees/content/logistics/ports/LogisticsPortBlockEntity.kt +++ b/src/main/kotlin/de/devin/cbbees/content/logistics/ports/LogisticsPortBlockEntity.kt @@ -6,12 +6,11 @@ import com.simibubi.create.foundation.blockEntity.SmartBlockEntity import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour import com.simibubi.create.foundation.blockEntity.behaviour.filtering.FilteringBehaviour import com.simibubi.create.foundation.blockEntity.behaviour.scrollValue.ScrollValueBehaviour +import de.devin.cbbees.content.domain.network.addNetworkTooltip import de.devin.cbbees.content.bee.MechanicalBeeEntity import de.devin.cbbees.content.domain.logistics.LogisticsPort import de.devin.cbbees.content.logistics.ports.LogisticPortBlock.Companion.PORT_STATE import de.devin.cbbees.util.extractExactly -import net.createmod.catnip.lang.Lang -import net.minecraft.ChatFormatting import net.minecraft.core.BlockPos import net.minecraft.core.HolderLookup import net.minecraft.nbt.CompoundTag @@ -36,7 +35,7 @@ class LogisticPortBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: Bl override val world: Level get() = getLevel()!! override val pos: BlockPos get() = blockPos - override var networkId: UUID = UUID.randomUUID() + override var networkId: UUID? = null set(value) { if (field == value) return val old = field @@ -108,9 +107,7 @@ class LogisticPortBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: Bl if (tag.hasUUID("lp_id")) { id = tag.getUUID("lp_id") } - if (tag.hasUUID("NetworkId")) { - networkId = tag.getUUID("NetworkId") - } + networkId = if (tag.hasUUID("NetworkId")) tag.getUUID("NetworkId") else null if (tag.contains("Filter")) { filterStack = ItemStack.parseOptional(registries, tag.getCompound("Filter")) } @@ -119,7 +116,7 @@ class LogisticPortBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: Bl override fun write(tag: CompoundTag, registries: HolderLookup.Provider, clientPacket: Boolean) { super.write(tag, registries, clientPacket) tag.putUUID("lp_id", id) - tag.putUUID("NetworkId", networkId) + networkId?.let { tag.putUUID("NetworkId", it) } if (!filterStack.isEmpty) { tag.put("Filter", filterStack.save(registries)) } @@ -284,11 +281,7 @@ class LogisticPortBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: Bl } override fun addToGoggleTooltip(tooltip: MutableList, isPlayerSneaking: Boolean): Boolean { - val network = network() - Lang.builder("cbbees").translate("gui.goggles.beehive.network") - .style(ChatFormatting.GRAY) - .add(Lang.builder("cbbees").text(network.name).style(ChatFormatting.GOLD)) - .forGoggles(tooltip) + addNetworkTooltip(tooltip) return true } } \ No newline at end of file diff --git a/src/main/kotlin/de/devin/cbbees/content/logistics/transport/TransportPortBlockEntity.kt b/src/main/kotlin/de/devin/cbbees/content/logistics/transport/TransportPortBlockEntity.kt index da6c9e7..aa716d1 100644 --- a/src/main/kotlin/de/devin/cbbees/content/logistics/transport/TransportPortBlockEntity.kt +++ b/src/main/kotlin/de/devin/cbbees/content/logistics/transport/TransportPortBlockEntity.kt @@ -6,6 +6,7 @@ import com.simibubi.create.content.redstone.link.LinkBehaviour import com.simibubi.create.foundation.blockEntity.SmartBlockEntity import com.simibubi.create.foundation.blockEntity.behaviour.BlockEntityBehaviour import com.simibubi.create.foundation.blockEntity.behaviour.scrollValue.ScrollValueBehaviour +import de.devin.cbbees.content.domain.network.addNetworkTooltip import de.devin.cbbees.content.domain.logistics.PortReservationManager import de.devin.cbbees.content.domain.logistics.TransportPort import de.devin.cbbees.content.logistics.ports.LogisticsPortSelectionValueBox @@ -37,7 +38,7 @@ class TransportPortBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: B override val world: Level get() = getLevel()!! override val pos: BlockPos get() = blockPos - override var networkId: UUID = UUID.randomUUID() + override var networkId: UUID? = null set(value) { if (field == value) return val old = field @@ -105,15 +106,13 @@ class TransportPortBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: B if (tag.hasUUID("tp_id")) { id = tag.getUUID("tp_id") } - if (tag.hasUUID("NetworkId")) { - networkId = tag.getUUID("NetworkId") - } + networkId = if (tag.hasUUID("NetworkId")) tag.getUUID("NetworkId") else null } override fun write(tag: CompoundTag, registries: HolderLookup.Provider, clientPacket: Boolean) { super.write(tag, registries, clientPacket) tag.putUUID("tp_id", id) - tag.putUUID("NetworkId", networkId) + networkId?.let { tag.putUUID("NetworkId", it) } } override fun getItemHandler(level: Level): IItemHandler? = getInventory(level) @@ -216,17 +215,13 @@ class TransportPortBlockEntity(type: BlockEntityType<*>, pos: BlockPos, state: B } override fun addToGoggleTooltip(tooltip: MutableList, isPlayerSneaking: Boolean): Boolean { - val network = network() val typeKey = if (isProvider()) "gui.goggles.transport.provider" else "gui.goggles.transport.requester" Lang.builder("cbbees").translate(typeKey) .style(ChatFormatting.GOLD) .forGoggles(tooltip) - Lang.builder("cbbees").translate("gui.goggles.beehive.network") - .style(ChatFormatting.GRAY) - .add(Lang.builder("cbbees").text(network.name).style(ChatFormatting.GOLD)) - .forGoggles(tooltip, 1) + addNetworkTooltip(tooltip, 1) return true } diff --git a/src/main/kotlin/de/devin/cbbees/gametest/network/NetworkLinkingTests.kt b/src/main/kotlin/de/devin/cbbees/gametest/network/NetworkLinkingTests.kt index c686e89..f5ca354 100644 --- a/src/main/kotlin/de/devin/cbbees/gametest/network/NetworkLinkingTests.kt +++ b/src/main/kotlin/de/devin/cbbees/gametest/network/NetworkLinkingTests.kt @@ -39,7 +39,7 @@ object NetworkLinkingTests { ) : BeeHive { override val id: UUID = UUID.randomUUID() override val world: Level get() = level - override var networkId: UUID = UUID.randomUUID() + override var networkId: UUID? = UUID.randomUUID() override fun getAvailableBeeCount(): Int = 0 override fun hasBeeOfType(beeType: BeeType): Boolean = false override fun getBeeContext(): BeeContext = BeeContext() @@ -61,7 +61,7 @@ object NetworkLinkingTests { ) : LogisticsPort { override val id: UUID = UUID.randomUUID() override val world: Level get() = level - override var networkId: UUID = UUID.randomUUID() + override var networkId: UUID? = UUID.randomUUID() override fun getPortType(): PortType = PortType.INSERT override fun getFilter(): ItemStack = ItemStack.EMPTY override fun isValidForPickup(): Boolean = true diff --git a/src/main/resources/assets/cbbees/lang/en_us.json b/src/main/resources/assets/cbbees/lang/en_us.json index b552bd4..b5837e3 100644 --- a/src/main/resources/assets/cbbees/lang/en_us.json +++ b/src/main/resources/assets/cbbees/lang/en_us.json @@ -63,6 +63,7 @@ "gui.cbbees.instruction.deconstruct": "Deconstruct Area", "cbbees.gui.goggles.beehive_stats": "Beehive Swarm Statistics", "cbbees.gui.goggles.beehive.network": "Network: ", + "cbbees.gui.goggles.network.none": "None", "cbbees.gui.goggles.beehive.flying": "Bees currently flying: ", "cbbees.gui.goggles.beehive.stored": "Bees currently stored: ", "cbbees.gui.goggles.beehive.capacity": "Max workforce capacity: ",