fixes to node ticking and beeping

This commit is contained in:
2026-04-22 22:30:37 +02:00
parent c98ec2fc99
commit 66097fdd6c
11 changed files with 258 additions and 26 deletions

View File

@@ -1,6 +1,7 @@
package org.neoflock.neocomputers
import dev.architectury.event.events.client.ClientLifecycleEvent
import dev.architectury.event.events.common.LifecycleEvent
import dev.architectury.event.events.common.PlayerEvent
import dev.architectury.event.events.common.TickEvent
import dev.architectury.networking.NetworkManager
@@ -64,6 +65,22 @@ object NeoComputers {
NodeSynchronizer.syncScreens()
}
TickEvent.PLAYER_POST.register {
Sounds.tickCustomSounds()
}
LifecycleEvent.SERVER_STARTING.register {
Networking.allNodes.remove()
Networking.wirelessNodes.remove()
Networking.channels.remove()
}
ClientLifecycleEvent.CLIENT_STARTED.register {
Networking.allNodes.remove()
Networking.wirelessNodes.remove()
Networking.channels.remove()
}
PlayerEvent.CLOSE_MENU.register {
player, menu ->
if(player is ServerPlayer) NodeSynchronizer.playerScreenClosed(player)
@@ -105,11 +122,18 @@ object NeoComputers {
scr.processScreenStatePacket(packet.buffer)
}
})
NetworkManager.registerReceiver(NetworkManager.s2c(),NodeSynchronizer.BeepDataPayload.TYPE, NodeSynchronizer.BeepDataPayload.CODEC, {
packet, ctx ->
// TODO: implement volume
Sounds.beep(packet.pos.center, packet.pattern, packet.freq, packet.duration.toMillis().toInt())
})
}}
EnvExecutor.runInEnv(Env.SERVER) {{
// https://github.com/architectury/architectury-api/issues/518
NetworkManager.registerS2CPayloadType(NodeSynchronizer.StatePayload.TYPE, NodeSynchronizer.StatePayload.CODEC)
NetworkManager.registerS2CPayloadType(NodeSynchronizer.ScreenPayload.TYPE, NodeSynchronizer.ScreenPayload.CODEC)
NetworkManager.registerS2CPayloadType(NodeSynchronizer.BeepDataPayload.TYPE, NodeSynchronizer.BeepDataPayload.CODEC)
}}
LOGGER.info("Registered!")

View File

@@ -37,7 +37,7 @@ class CaseBlock() : NodeBlock(Properties.of().sound(SoundType.METAL).lightLevel(
}
}
override fun newBlockEntity(blockPos: BlockPos, blockState: BlockState) = CaseBlockEntity(blockPos, blockState)
override fun newBlockEntity(blockPos: BlockPos, blockState: BlockState) = CaseBlockEntity(blockPos, blockState).initNetworking()
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block?, BlockState?>) {
builder.add(COMPUTER_RUNNING)

View File

@@ -25,6 +25,7 @@ import net.minecraft.world.level.block.state.BlockState
import org.neoflock.neocomputers.NeoComputers
import org.neoflock.neocomputers.network.Networking
import org.neoflock.neocomputers.network.PowerRole
import java.time.Duration
object NodeSynchronizer {
class StatePayload(var blockPos: BlockPos, var buffer: FriendlyByteBuf): CustomPacketPayload {
@@ -71,8 +72,8 @@ object NodeSynchronizer {
class ScreenDataPayload(var entityTypeWireID: String, var buffer: FriendlyByteBuf): CustomPacketPayload {
companion object {
val SCREEN_SYNC_ID = ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "screen_data")
val TYPE = CustomPacketPayload.Type<ScreenDataPayload>(SCREEN_SYNC_ID)
val SCREEN_DATA_ID = ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "screen_data")
val TYPE = CustomPacketPayload.Type<ScreenDataPayload>(SCREEN_DATA_ID)
val CODEC = object : StreamCodec<RegistryFriendlyByteBuf, ScreenDataPayload> {
override fun decode(buf: RegistryFriendlyByteBuf): ScreenDataPayload {
val id = buf.readByteArray().decodeToString()
@@ -90,6 +91,33 @@ object NodeSynchronizer {
override fun type() = TYPE
}
class BeepDataPayload(val pos: BlockPos, val pattern: String, val freq: Int, val duration: Duration, val volume: Double): CustomPacketPayload {
companion object {
val BEEP_DATA_ID = ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "beep_data")
val TYPE = CustomPacketPayload.Type<BeepDataPayload>(BEEP_DATA_ID)
val CODEC = object : StreamCodec<RegistryFriendlyByteBuf, BeepDataPayload> {
override fun decode(buf: RegistryFriendlyByteBuf): BeepDataPayload {
val pos = buf.readBlockPos()
val pattern = buf.readUtf()
val freq = buf.readVarInt()
val duration = buf.readVarLong()
val volume = buf.readDouble()
return BeepDataPayload(pos, pattern, freq, Duration.ofMillis(duration), volume)
}
override fun encode(buf: RegistryFriendlyByteBuf, payload: BeepDataPayload) {
buf.writeBlockPos(payload.pos)
buf.writeUtf(payload.pattern)
buf.writeVarInt(payload.freq)
buf.writeVarLong(payload.duration.toMillis())
buf.writeDouble(payload.volume)
}
}
}
override fun type() = TYPE
}
val screenMap = mutableMapOf<ServerPlayer, NodeBlockEntity>()
fun playerScreenClosed(player: ServerPlayer) {
@@ -113,6 +141,14 @@ object NodeSynchronizer {
fun sendScreenInteraction(friendlyByteBuf: FriendlyByteBuf) {
NetworkManager.sendToServer(ScreenDataPayload("", friendlyByteBuf))
}
fun emitBeep(level: Level, beepDataPayload: BeepDataPayload) {
if(level is ServerLevel) {
level.players().forEach {
NetworkManager.sendToPlayer(it, beepDataPayload)
}
}
}
}
abstract class NodeBlockEntity(blockEntityType: BlockEntityType<*>, blockPos: BlockPos, blockState: BlockState) : BlockEntity(blockEntityType, blockPos, blockState) {
@@ -177,7 +213,7 @@ abstract class NodeBlockEntity(blockEntityType: BlockEntityType<*>, blockPos: Bl
stateIsDirty = true
}
fun needsSynchronization() = stateIsDirty
open fun needsSynchronization() = stateIsDirty
open fun tickNode(level: Level) {
if(!level.isClientSide) {
@@ -203,9 +239,6 @@ abstract class NodeBlockEntity(blockEntityType: BlockEntityType<*>, blockPos: Bl
override fun setRemoved() {
super.setRemoved()
if(node.powerRole == PowerRole.GENERATOR) {
NeoComputers.LOGGER.info("removed generator ${node.address}")
}
Networking.removeNode(node)
}
@@ -229,7 +262,8 @@ abstract class NodeBlock(properties: Properties = Properties.of()): BaseBlock(pr
): BlockEntityTicker<T>? {
return object : BlockEntityTicker<T> {
override fun tick(level: Level, blockPos: BlockPos, blockState: BlockState, blockEntity: T) {
if(blockEntity !is NodeBlockEntity) return;
if(blockEntity !is NodeBlockEntity) return
if(Networking.getNode(blockEntity.node.address) == null) blockEntity.initNetworking()
blockEntity.tickNode(level)
}
}

View File

@@ -1,10 +1,7 @@
package org.neoflock.neocomputers.entity
import net.minecraft.client.Minecraft
import net.minecraft.client.resources.sounds.BiomeAmbientSoundsHandler
import net.minecraft.client.resources.sounds.EntityBoundSoundInstance
import net.minecraft.client.resources.sounds.SoundInstance
import net.minecraft.client.sounds.LoopingAudioStream
import net.minecraft.core.BlockPos
import net.minecraft.core.Direction
import net.minecraft.core.HolderLookup
@@ -14,7 +11,6 @@ import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.chat.Component
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundSource
import net.minecraft.world.Container
import net.minecraft.world.ContainerHelper
import net.minecraft.world.MenuProvider
import net.minecraft.world.entity.player.Inventory
@@ -25,6 +21,7 @@ import net.minecraft.world.level.block.state.BlockState
import org.neoflock.neocomputers.NeoComputers
import org.neoflock.neocomputers.block.CaseBlock
import org.neoflock.neocomputers.block.NodeBlockEntity
import org.neoflock.neocomputers.block.NodeSynchronizer
import org.neoflock.neocomputers.block.dirToIdx
import org.neoflock.neocomputers.gui.menu.CaseMenu
import org.neoflock.neocomputers.item.ComponentItem
@@ -45,7 +42,7 @@ class CaseBlockEntity(blockPos: BlockPos, blockState: BlockState): NodeBlockEnti
var soundInstance: SoundInstance? = null
override val node = object : Networking.Node() {
override var powerRole = PowerRole.STORAGE
override var powerRole = PowerRole.CONSUMER
override var energyCapacity: Long = 500
}
@@ -130,7 +127,6 @@ class CaseBlockEntity(blockPos: BlockPos, blockState: BlockState): NodeBlockEnti
isOn = value
val world = level ?: return
blockState?.setValue(CaseBlock.COMPUTER_RUNNING, isOn)
if(value) beepAsync(8000, Duration.ofSeconds(1), 1.0)
if(world.isClientSide) {
if(value) {
soundInstance = ComputerRunningSoundInstance(this, Sounds.COMPUTER_RUNNING.get(), SoundSource.AMBIENT)
@@ -147,29 +143,50 @@ class CaseBlockEntity(blockPos: BlockPos, blockState: BlockState): NodeBlockEnti
}
override fun start(): Boolean {
if(isOn) return true
err = null
val archs = getMachineArchitectures()
// Beep patterns taken from https://github.com/MightyPirates/OpenComputers/blob/571482db88080d56329e8f8cf0db2a90825bf1d7/src/main/scala/li/cil/oc/server/machine/Machine.scala
if(archs.isEmpty()) {
crash("no cpu")
beepAsync("-..")
return false
}
if(getMachineComponentsUsed() > getMachineComponentsTotal()) {
crash("too many components")
beepAsync("-..")
return false
}
if(node.energy < 100) {
crash("not enough energy")
// we add a beep for the special case where we do have a little bit of energy :P
if(node.energy > 0) beepAsync("..")
return false
}
if(getMachineMemoryTotal() == 0L) {
crash("no memory provided")
beepAsync("-.")
return false
}
if(arch !in archs) {
// Just pick one!
arch = archs.first()
}
beepAsync(".")
setRunning(true)
return isOn
}
override fun stop(): Boolean {
if(!isOn) return false
setRunning(false)
return isOn
}
override fun crash(error: String): Boolean {
if(isOn) {
beepAsync("--")
}
setRunning(false)
err = error
return true
@@ -190,8 +207,8 @@ class CaseBlockEntity(blockPos: BlockPos, blockState: BlockState): NodeBlockEnti
return old
}
override fun beepAsync(frequency: Int, duration: Duration, volume: Double): Boolean {
NeoComputers.LOGGER.warn("beep not yet implemented")
override fun beepAsync(pattern: String, frequency: Int, duration: Duration, volume: Double): Boolean {
NodeSynchronizer.emitBeep(level!!, NodeSynchronizer.BeepDataPayload(getMachineBlockPosition(), pattern, frequency, duration, volume))
return true
}
@@ -231,10 +248,6 @@ class CaseBlockEntity(blockPos: BlockPos, blockState: BlockState): NodeBlockEnti
override fun getDisplayName(): Component? = Component.literal("Computer")
override fun createMenu(i: Int, inventory: Inventory, player: Player) = CaseMenu(i, inventory, this)
override fun setChanged() {
super.setChanged()
}
override fun setRemoved() {
setRunning(false)
super.setRemoved()

View File

@@ -19,7 +19,12 @@ interface MachineEntity {
fun getMachineBlockPosition(): BlockPos
fun getMachineLevel(): Level
fun beepAsync(frequency: Int, duration: Duration, volume: Double): Boolean
// Pattern can have dots (.), dashes (-) and spaces ( ).
// Each character is duration long, and has a 50ms break.
// For non-short ones, which are typically reserved only for hardware interactions,
// the duration is doubled.
// Architectures should only use short ones.
fun beepAsync(pattern: String, frequency: Int = 1000, duration: Duration = Duration.ofMillis(200), volume: Double = 1.0): Boolean
fun isRunning(): Boolean
fun start(): Boolean

View File

@@ -9,11 +9,13 @@ import net.minecraft.network.chat.Component
import net.minecraft.resources.ResourceLocation
import net.minecraft.world.entity.player.Inventory
import net.minecraft.world.inventory.tooltip.TooltipComponent
import net.minecraft.world.phys.Vec3
import org.neoflock.neocomputers.NeoComputers
import org.neoflock.neocomputers.block.NodeSynchronizer
import org.neoflock.neocomputers.gui.menu.CaseMenu
import org.neoflock.neocomputers.gui.widget.ButtonSprites
import org.neoflock.neocomputers.gui.widget.ImagerButton
import org.neoflock.neocomputers.sounds.Sounds
import org.neoflock.neocomputers.utils.Formatting
import org.neoflock.neocomputers.utils.GenericContainerScreen
import java.util.Optional

View File

@@ -23,4 +23,7 @@ object DataComponents {
DataComponentType.builder<Int>().persistent(Codec.INT).build())
val EEPROM_DATASIZE = Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "eeprom_datasize"),
DataComponentType.builder<Int>().persistent(Codec.INT).build())
val TUNNEL_CHANNEL = Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "tunnel_channel"),
DataComponentType.builder<String>().persistent(Codec.STRING).build())
}

View File

@@ -7,9 +7,8 @@ import net.minecraft.world.item.TooltipFlag
import org.neoflock.neocomputers.entity.MachineEntity
import org.neoflock.neocomputers.gui.widget.ComponentRoles
import org.neoflock.neocomputers.network.Networking
import org.neoflock.neocomputers.utils.Formatting
class TunnelCard: Item(Properties()), ComponentItem {
class TunnelCard: Item(Properties().component(DataComponents.TUNNEL_CHANNEL, "creative")), ComponentItem {
// yes, we're counting TUNNEL as a conventional networking card
override fun getComponentRoles(itemStack: ItemStack): Set<String> = setOf(ComponentRoles.CARD, ComponentRoles.NETWORK)
@@ -33,6 +32,7 @@ class TunnelCard: Item(Properties()), ComponentItem {
val addr = itemStack.get(DataComponents.ADDRESS)
val addrComp = if(addr == null) Component.translatable("neocomputers.noaddr") else Component.literal(addr)
list.addLast(addrComp)
list.addLast(Component.translatable("neocomputers.tunnel.channel", itemStack.get(DataComponents.TUNNEL_CHANNEL) ?: "creative"))
// TODO: show max packet size and whatnot
}
super.appendHoverText(itemStack, tooltipContext, list, tooltipFlag)

View File

@@ -288,7 +288,7 @@ object Networking {
// TODO: use setter, more convenient
fun changeNodeAddress(node: Node, address: UUID) {
if(node.address.equals(address)) return
NeoComputers.LOGGER.info("remapping node from ${node.address} to $address")
if(node.address !in allNodes.get()) return
allNodes.get().remove(node.address)
node.address = address
allNodes.get()[address] = node
@@ -296,7 +296,6 @@ object Networking {
fun addNode(node: Node) {
if(node.address in allNodes.get()) return
NeoComputers.LOGGER.info("adding node ${node.address}")
allNodes.get()[node.address] = node
if(node is WirelessEndpoint) {
wirelessNodes.get().add(node);
@@ -311,7 +310,6 @@ object Networking {
fun removeNode(node: Node) {
if(node.address !in allNodes.get()) return
NeoComputers.LOGGER.info("removing node ${node.address}")
allNodes.get().forEach { it.value.onNodeRemoved(node) }
// toList() in order to copy it
node.connections.toList().forEach {

View File

@@ -1,10 +1,20 @@
package org.neoflock.neocomputers.sounds
import dev.architectury.registry.registries.DeferredRegister
import net.minecraft.client.Minecraft
import net.minecraft.core.registries.Registries
import net.minecraft.resources.ResourceLocation
import net.minecraft.sounds.SoundEvent
import net.minecraft.world.phys.Vec3
import org.lwjgl.BufferUtils
import org.neoflock.neocomputers.NeoComputers
import org.lwjgl.openal.AL10
import java.nio.ByteBuffer
import kotlin.experimental.xor
import kotlin.math.PI
import kotlin.math.max
import kotlin.math.sign
import kotlin.math.sin
object Sounds {
val SOUNDS = DeferredRegister.create(NeoComputers.MODID, Registries.SOUND_EVENT)!!
@@ -14,4 +24,145 @@ object Sounds {
fun registerSound(name: String) = SOUNDS.register(name) {
SoundEvent.createVariableRangeEvent(ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, name))
}!!
val BEEP_SAMPLERATE = 44100
val BEEP_AMPLITUDE = 32f
val BEEP_MAXDIST = 16f
// Also largely taken from https://github.com/MightyPirates/OpenComputers/blob/571482db88080d56329e8f8cf0db2a90825bf1d7/src/main/scala/li/cil/oc/util/Audio.scala
val allSounds = ThreadLocal.withInitial { mutableListOf<CustomSoundBuffer>() }
class CustomSoundBuffer {
var dead: Boolean = true
var buffer: Int = -1
var source: Int = -1
fun start(x: Float, y: Float, z: Float, data: ByteBuffer, gain: Float): Int? {
// clear errors or smth idk
AL10.alGetError()
// written in a C style by a C dev
// all this work on a JVM project and I'm still writing C
// would be better if Kotlin had goto btw just saying
val ok = AL10.AL_NO_ERROR
var err = ok
buffer = AL10.alGenBuffers()
err = AL10.alGetError()
if(err != ok) return err
AL10.alBufferData(buffer, AL10.AL_FORMAT_MONO8, data, BEEP_SAMPLERATE)
err = AL10.alGetError()
if(err != ok) {
AL10.alDeleteBuffers(buffer)
return err
}
source = AL10.alGenSources()
err = AL10.alGetError()
if(err != ok) {
AL10.alDeleteBuffers(buffer)
return err
}
AL10.alSourceQueueBuffers(source, buffer)
err = AL10.alGetError()
if(err != ok) {
AL10.alDeleteBuffers(buffer)
AL10.alDeleteSources(source)
return err
}
AL10.alSource3f(source, AL10.AL_POSITION, x, y, z)
AL10.alSourcef(source, AL10.AL_REFERENCE_DISTANCE, BEEP_MAXDIST)
AL10.alSourcef(source, AL10.AL_MAX_DISTANCE, BEEP_MAXDIST)
AL10.alSourcef(source, AL10.AL_GAIN, gain * 0.3f)
err = AL10.alGetError()
if(err != ok) {
AL10.alDeleteBuffers(buffer)
AL10.alDeleteSources(source)
return err
}
AL10.alSourcePlay(source)
err = AL10.alGetError()
if(err != ok) {
AL10.alDeleteBuffers(buffer)
AL10.alDeleteSources(source)
return err
}
dead = false
return null
}
fun checkDone(): Boolean {
if(dead) return true
if(AL10.alGetSourcei(source, AL10.AL_SOURCE_STATE) == AL10.AL_PLAYING) return false
NeoComputers.LOGGER.info("sound buffer stopped")
dead = true
AL10.alDeleteSources(source)
AL10.alDeleteBuffers(buffer)
return true
}
}
fun beep(pos: Vec3, pattern: String, frequency: Int = 1000, duration: Int = 200) {
NeoComputers.LOGGER.info("Beep: $pattern, $frequency Hz, $duration ms")
val mc = Minecraft.getInstance()
val playerPos = mc.player?.position() ?: pos
val distanceBasedGain = max(0.0, 1 - pos.distanceTo(playerPos) / BEEP_MAXDIST).toFloat()
val volume = 1.0
val gain = distanceBasedGain * volume
if (gain <= 0 || BEEP_AMPLITUDE <= 0) return
// Algorithm effectively ported over from https://github.com/MightyPirates/OpenComputers/blob/571482db88080d56329e8f8cf0db2a90825bf1d7/src/main/scala/li/cil/oc/util/Audio.scala
// We do add support for spaces tho
val charArr = pattern.toCharArray()
val sampleCounts = charArr.map { if(it == '.') duration else 2 * duration }.map { it * BEEP_SAMPLERATE / 1000 }
val pauseSample = 50 * BEEP_SAMPLERATE / 1000
val finalBuf = BufferUtils.createByteBuffer(sampleCounts.sum() + pauseSample * sampleCounts.lastIndex)
val step = frequency.toFloat() / BEEP_SAMPLERATE
var off = 0f
for((i, sampleCount) in sampleCounts.withIndex()) {
if(charArr[i] == ' ') {
for(sample in 0..<sampleCount) {
finalBuf.put(127)
}
} else {
for(sample in 0..<sampleCount) {
val angle = 2 * PI * off
val value = (sin(angle).sign * BEEP_AMPLITUDE).toInt().toByte().xor(0x80.toByte())
off += step
if(off > 1) off -= 1f
finalBuf.put(value)
}
}
if(finalBuf.hasRemaining()) {
for(sample in 0..<pauseSample) {
finalBuf.put(127)
}
}
}
finalBuf.rewind()
val l = mutableListOf<Int>()
while(finalBuf.hasRemaining()) l.addLast(finalBuf.get().toInt())
finalBuf.rewind()
NeoComputers.LOGGER.info("$l")
val sound = CustomSoundBuffer()
val soundErr = sound.start(pos.x.toFloat(), pos.y.toFloat(), pos.z.toFloat(), finalBuf, gain.toFloat())
if(soundErr != null) {
NeoComputers.LOGGER.error("Playing beep failed, OpenAL exit code of $soundErr")
return
}
NeoComputers.LOGGER.info("Beeping with ${finalBuf.capacity()} samples")
allSounds.get().addLast(sound)
}
fun tickCustomSounds() {
allSounds.get().removeIf { it.checkDone() }
}
}