This commit is contained in:
2026-04-22 22:34:33 +02:00
17 changed files with 423 additions and 77 deletions

View File

@@ -3,9 +3,11 @@ package org.neoflock.neocomputers
import com.mojang.blaze3d.vertex.DefaultVertexFormat
import com.mojang.blaze3d.vertex.VertexFormat
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
import dev.architectury.platform.Platform
import net.minecraft.resources.ResourceLocation
import org.neoflock.neocomputers.block.Blocks
import org.neoflock.neocomputers.entity.BlockEntities
@@ -84,6 +86,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)
@@ -94,6 +112,10 @@ object NeoComputers {
NodeSynchronizer.playerScreenClosed(player)
}
// networking has no way to define a C2S packet type, so we need the listener on both
// however, defining it separately on both breaks both ends
// so we define it once, but on both platforms
if(Platform.getEnvironment() == Env.CLIENT || Platform.getEnvironment() == Env.SERVER) {
NetworkManager.registerReceiver(NetworkManager.c2s(),NodeSynchronizer.ScreenDataPayload.TYPE, NodeSynchronizer.ScreenDataPayload.CODEC, {
packet, ctx ->
val player = ctx.player
@@ -101,6 +123,7 @@ object NeoComputers {
NodeSynchronizer.screenMap[player]?.processScreenInteraction(player, packet.buffer)
}
})
}
// we have to do this because the datagen task runs in the physical server
EnvExecutor.runInEnv(Env.CLIENT) {{
@@ -120,11 +143,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

@@ -24,6 +24,8 @@ import net.minecraft.world.level.block.entity.BlockEntityType
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 {
@@ -70,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()
@@ -89,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) {
@@ -112,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) {
@@ -134,7 +171,7 @@ abstract class NodeBlockEntity(blockEntityType: BlockEntityType<*>, blockPos: Bl
// runs on the client, meant to decode server state packets to synchronize client state
open fun syncWithUpstream(packet: FriendlyByteBuf) {
node.address = packet.readUUID()
Networking.changeNodeAddress(node, packet.readUUID())
node.energy = packet.readLong()
node.energyCapacity = packet.readLong()
node.reachability = packet.readEnum(node.reachability.javaClass)
@@ -176,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) {
@@ -225,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

@@ -87,5 +87,6 @@ object BlockEntities {
PowerManager.registerPowerBlockEntity(CAPACITOR3_ENTITY.get())
PowerManager.registerPowerBlockEntity(SOLARGEN_ENTITY.get())
PowerManager.registerPowerBlockEntity(COMBUSTGEN_ENTITY.get())
PowerManager.registerPowerBlockEntity(CASE_ENTITY.get())
}
}

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
@@ -40,10 +37,12 @@ class CaseBlockEntity(blockPos: BlockPos, blockState: BlockState): NodeBlockEnti
val stacks: NonNullList<ItemStack> = NonNullList<ItemStack>.withSize(7, ItemStack.EMPTY)
var isOn = false
var err: String? = null
var arch = "Lua 5.3"
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
}
@@ -70,6 +69,14 @@ class CaseBlockEntity(blockPos: BlockPos, blockState: BlockState): NodeBlockEnti
override fun encodeScreenData(player: ServerPlayer, packet: FriendlyByteBuf) {
super.encodeScreenData(player, packet)
packet.writeBoolean(isOn)
packet.writeByteArray((err ?: "").encodeToByteArray())
packet.writeLong(node.energy)
packet.writeLong(node.energyCapacity)
packet.writeLong(getMachineMemoryUsed())
packet.writeLong(getMachineMemoryTotal())
packet.writeLong(getMachineComponentsUsed())
packet.writeLong(getMachineComponentsTotal())
packet.writeUtf(arch)
}
val redstoneIn = Array(Direction.entries.size) {0}
@@ -136,21 +143,56 @@ 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 {
NeoComputers.LOGGER.warn("Crashing cases is not implemented yet lol")
return false
if(isOn) {
beepAsync("--")
}
setRunning(false)
err = error
return true
}
override fun getLastError(): String? = null
override fun getLastError(): String? = err
override fun getMachineNode(): Networking.Node = node
@@ -165,19 +207,29 @@ 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
}
override fun getMachineMemoryTotal(): Long = stacks.mapNotNull { (it.item as? ComponentItem)?.getMemoryCapacity(it) }.sum().toLong()
override fun getMachineMemoryUsed(): Long = 0
override fun getMachineComponentsUsed(): Long = node.connections.size.toLong()
override fun getMachineComponentsUsed(): Long = node.getReachable().size.toLong()
override fun getMachineComponentsTotal(): Long = stacks.mapNotNull { (it.item as? ComponentItem)?.getComponentCapacity(it) }.sum().toLong()
override fun getMachineArchitecture() = arch
override fun getMachineArchitectures() = stacks.mapNotNull { (it.item as? ComponentItem)?.getArchitecturesProvided(it) }.flatten().toSet()
override fun setMachineArchitecture(arch: String) {
if(this.arch == arch) return
this.arch = arch
if(isRunning()) {
stop()
start()
}
}
override fun getItems(): NonNullList<ItemStack> = stacks
override fun stillValid(player: Player): Boolean = true
override fun stillValid(player: Player): Boolean = !this.isRemoved
override fun loadAdditional(compoundTag: CompoundTag, provider: HolderLookup.Provider) {
super.loadAdditional(compoundTag, provider)
@@ -196,15 +248,19 @@ 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 canPlaceItem(i: Int, itemStack: ItemStack): Boolean = false
override fun canTakeItem(container: Container, i: Int, itemStack: ItemStack): Boolean = false
override fun setChanged() {
super.setChanged()
}
override fun setRemoved() {
setRunning(false)
super.setRemoved()
}
override fun tickNode(level: Level) {
super.tickNode(level)
if(!level.isClientSide) {
if (isRunning()) {
if (!node.consumeEnergy(1)) {
crash("out of energy")
}
}
}
}
}

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
@@ -34,6 +39,9 @@ interface MachineEntity {
fun getMachineMemoryUsed(): Long
fun getMachineComponentsUsed(): Long
fun getMachineComponentsTotal(): Long
fun getMachineArchitecture(): String
fun getMachineArchitectures(): Set<String>
fun setMachineArchitecture(arch: String)
// Redstone signals
fun getRedstoneInput(direction: Direction): Int

View File

@@ -20,20 +20,20 @@ open class CaseMenu : GenericContainerMenu {
constructor(i: Int, inv: Inventory) : this(i, inv, SimpleContainer(7))
constructor(i: Int, inv: Inventory, container: Container) : super(Menus.CASE_MENU.get(), i, container) {
this.addInventorySlots(inv, 8, 84)
val machine = container as? CaseBlockEntity
this.addSlot(ComponentSlot(this.container, 0, 20, 34, machine, eepromRequirement))
this.addSlot(ComponentSlot(container, 0, 20, 34, machine, eepromRequirement))
var i = 1
for ((col, slotCol) in slotRequirements.withIndex()) {
for ((row, slotReq) in slotCol.withIndex()) {
this.addSlot(ComponentSlot(this.container, i, 98+(col*22), 18*(row+1)-2, machine, slotReq))
this.addSlot(ComponentSlot(container, i, 98+(col*22), 18*(row+1)-2, machine, slotReq))
i++
}
}
this.addInventorySlots(inv, 8, 84)
// for (int col=1; col<4; col++) {
// for (int row=1; row<4; row++) {
// int i = (row-1)*3+(col-1);

View File

@@ -1,26 +1,24 @@
package org.neoflock.neocomputers.gui.screen;
import com.mojang.blaze3d.vertex.BufferBuilder
import com.mojang.blaze3d.vertex.DefaultVertexFormat
import com.mojang.blaze3d.vertex.Tesselator
import com.mojang.blaze3d.vertex.VertexFormat
import io.netty.buffer.Unpooled
import net.minecraft.ChatFormatting
import net.minecraft.client.Minecraft
import net.minecraft.client.gui.GuiGraphics
import net.minecraft.client.gui.components.Button
import net.minecraft.client.gui.components.ImageButton
import net.minecraft.client.gui.components.WidgetSprites
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen
import net.minecraft.network.FriendlyByteBuf
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
class CaseScreen : GenericContainerScreen<CaseMenu> {
private val PCB: ResourceLocation = ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "textures/gui/computer.png")
@@ -34,11 +32,49 @@ class CaseScreen : GenericContainerScreen<CaseMenu> {
override fun shouldCenterTitle(): Boolean = false
var isOn = false
var lastError: String? = null
var energy: Long = 0L
var maxEnergy: Long = 0L
var memory: Long = 0L
var maxMemory: Long = 0L
var components: Long = 0L
var maxComponents: Long = 0L
var arch = ""
override fun processScreenStatePacket(buf: FriendlyByteBuf) {
super.processScreenStatePacket(buf)
isOn = buf.readBoolean()
btn?.pressed = isOn
val error = buf.readByteArray().decodeToString()
if(error.isEmpty()) {
lastError = null
} else {
lastError = error
}
energy = buf.readLong()
maxEnergy = buf.readLong()
memory = buf.readLong()
maxMemory = buf.readLong()
components = buf.readLong()
maxComponents = buf.readLong()
arch = buf.readUtf()
}
fun computeButtonTooltip(): List<Component> {
val msgs = mutableListOf(Component.literal("Computer " + if(isOn) "ON" else "OFF").withStyle(if(isOn) ChatFormatting.GREEN else ChatFormatting.RED))
if(lastError != null) {
msgs.addLast(Component.literal("Error: ").withStyle(ChatFormatting.RED).append(Component.literal(lastError!!)))
}
if(arch.isNotEmpty()) {
msgs.addLast(Component.literal("Architecture: $arch"))
}
if(hasShiftDown()) {
msgs.addLast(Component.literal("Energy: $energy / $maxEnergy J").withStyle(if(energy < 100) ChatFormatting.RED else ChatFormatting.WHITE))
msgs.addLast(Component.literal("Memory: ${Formatting.formatMemory(memory)} / ${Formatting.formatMemory(maxMemory)}"))
msgs.addLast(Component.literal("Components: $components / $maxComponents").withStyle(if(components <= maxComponents) ChatFormatting.WHITE else ChatFormatting.RED))
}
return msgs
}
constructor(abstractContainerMenu: CaseMenu, inventory: Inventory, component: Component) : super(abstractContainerMenu, inventory, component) {
@@ -64,14 +100,20 @@ class CaseScreen : GenericContainerScreen<CaseMenu> {
guiGraphics.blit(PCB, relX, relY, 0, 0, this.imageWidth, this.imageHeight)
}
override fun renderCustomOverlay(graphics: GuiGraphics, mouseX: Int, mouseY: Int, blend: Float) {
super.renderCustomOverlay(graphics, mouseX, mouseY, blend)
if(btn!!.isHovered) {
graphics.renderTooltip(this.font, computeButtonTooltip(), Optional.empty<TooltipComponent>(), mouseX, mouseY)
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { // todo: make a better widget system than mojang, practically not even using the fact it's a widget atp
NeoComputers.LOGGER.info(String.format("btn: %d %d %d %d, mouse %s %s", btn!!.x, btn!!.y, btn!!.x+btn!!.width, btn!!.y+btn!!.height, mouseX.toString(), mouseY.toString()))
if (button != 0) return false
if (btn!!.x < mouseX.toInt() && mouseX.toInt() < btn!!.x+btn!!.width && btn!!.y < mouseY.toInt() && mouseY.toInt() < btn!!.y+btn!!.height) {
if (button == 0 && btn!!.isHovered) {
btn!!.playDownSound(Minecraft.getInstance().soundManager)
btn!!.onClick(mouseX, mouseY)
return true
} else return false
}
return super.mouseClicked(mouseX, mouseY, button)
}
}

View File

@@ -13,6 +13,8 @@ open class CPUItem(val tier: Int, val maxComponents: Int): Item(Item.Properties(
override fun getComponentCapacity(itemStack: ItemStack): Int = maxComponents
override fun getArchitecturesProvided(itemStack: ItemStack): Set<String> = setOf("Lua 5.3")
override fun toComponentNode(itemStack: ItemStack, machine: MachineEntity): Networking.Node? = null
}

View File

@@ -14,6 +14,7 @@ interface ComponentItem {
// Get machine properties they can influence
fun getMemoryCapacity(itemStack: ItemStack): Int = 0
fun getComponentCapacity(itemStack: ItemStack): Int = 0
fun getArchitecturesProvided(itemStack: ItemStack): Set<String> = setOf()
// Component placed, node must now exist
fun whenComponentPlaced(itemStack: ItemStack, machine: MachineEntity, newRole: String) {

View File

@@ -19,4 +19,11 @@ object DataComponents {
DataComponentType.builder<ByteBuffer>().persistent(Codec.BYTE_BUFFER).build())
val EEPROM_DATA = Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "eeprom_data"),
DataComponentType.builder<ByteBuffer>().persistent(Codec.BYTE_BUFFER).build())
val EEPROM_CODESIZE = Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "eeprom_codesize"),
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

@@ -13,6 +13,8 @@ import java.nio.ByteBuffer
fun getEEPROMProperties(codeCap: Int, dataCap: Int): Item.Properties = Item.Properties()
.component(DataComponents.EEPROM_CODE, ByteBuffer.allocate(codeCap))
.component(DataComponents.EEPROM_DATA, ByteBuffer.allocate(dataCap))
.component(DataComponents.EEPROM_CODESIZE, 0)
.component(DataComponents.EEPROM_DATASIZE, 0)
.component(DataComponents.LABEL, "")
.component(DataComponents.READONLY, false)
@@ -39,8 +41,8 @@ open class EEPROMItem(val tier: Int, val codeCapacity: Int, val dataCapacity: In
tooltipFlag: TooltipFlag
) {
if(tooltipFlag.isAdvanced) {
val codeSize = itemStack.get(DataComponents.EEPROM_CODE)?.position() ?: 0
val dataSize = itemStack.get(DataComponents.EEPROM_DATA)?.position() ?: 0
val codeSize = itemStack.get(DataComponents.EEPROM_CODESIZE) ?: 0
val dataSize = itemStack.get(DataComponents.EEPROM_DATASIZE) ?: 0
val addr = itemStack.get(DataComponents.ADDRESS)
val readonly = itemStack.get(DataComponents.READONLY) ?: false
val addrComp = if(addr == null) Component.translatable("neocomputers.noaddr") else Component.literal(addr)

View File

@@ -72,6 +72,7 @@ object Tabs {
codeBuf.put(code)
luaBios.set(DataComponents.LABEL, "Lua BIOS")
luaBios.set(DataComponents.EEPROM_CODE, codeBuf)
luaBios.set(DataComponents.EEPROM_CODESIZE, code.size)
output.accept(luaBios)
} while(false)
}

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

@@ -246,8 +246,8 @@ object Networking {
abstract fun receiveWireless(message: Message, emitter: WirelessEndpoint)
}
val wirelessNodes = mutableSetOf<WirelessEndpoint>()
val allNodes = mutableMapOf<UUID, Node>()
val wirelessNodes = ThreadLocal.withInitial { mutableSetOf<WirelessEndpoint>() }
val allNodes = ThreadLocal.withInitial { mutableMapOf<UUID, Node>() }
// node may differ from message.sender in the case of relays,
// as they might have DIRECT reachability but
@@ -267,7 +267,7 @@ object Networking {
val startPos = starter.getPosition();
val startDim = starter.getDimension();
val range = starter.getRange();
wirelessNodes.forEach {
wirelessNodes.get().forEach {
if(it.getDimension() != startDim) return;
val pos = it.getPosition();
val d = distanceBetween(startPos, pos);
@@ -279,27 +279,29 @@ object Networking {
}
fun tickAllNodes() {
allNodes.forEach { it.value.tick() }
allNodes.get().forEach { it.value.tick() }
tickCount++
}
fun getNode(address: UUID): Node? = allNodes[address]
fun getNode(address: UUID): Node? = allNodes.get()[address]
// TODO: use setter, more convenient
fun changeNodeAddress(node: Node, address: UUID) {
allNodes.remove(node.address)
if(node.address.equals(address)) return
if(node.address !in allNodes.get()) return
allNodes.get().remove(node.address)
node.address = address
allNodes[address] = node
allNodes.get()[address] = node
}
fun addNode(node: Node) {
if(node.address in allNodes) return;
allNodes[node.address] = node
if(node.address in allNodes.get()) return
allNodes.get()[node.address] = node
if(node is WirelessEndpoint) {
wirelessNodes.add(node);
wirelessNodes.get().add(node);
}
// notify at the end so it is notified of its own creation
allNodes.forEach { it.value.onNodeAdded(node) }
allNodes.get().forEach { it.value.onNodeAdded(node) }
}
fun addNodes(vararg nodes: Node) {
@@ -307,16 +309,16 @@ object Networking {
}
fun removeNode(node: Node) {
if(node.address !in allNodes) return
allNodes.forEach { it.value.onNodeRemoved(node) }
if(node.address !in allNodes.get()) return
allNodes.get().forEach { it.value.onNodeRemoved(node) }
// toList() in order to copy it
node.connections.toList().forEach {
node.disconnectFrom(it)
}
// actually remove at the end so it can listen to its own removal
allNodes.remove(node.address)
allNodes.get().remove(node.address)
if(node is WirelessEndpoint) {
wirelessNodes.remove(node);
wirelessNodes.get().remove(node);
}
}
@@ -324,25 +326,28 @@ object Networking {
nodes.forEach { removeNode(it) }
}
val channels = mutableMapOf<String, MutableSet<Node>>();
val channels = ThreadLocal.withInitial { mutableMapOf<String, MutableSet<Node>>() }
fun addToChannel(channel: String, node: Node) {
if(!channels.containsKey(channel)) {
channels[channel] = mutableSetOf();
val localChannels = channels.get()
if(!localChannels.containsKey(channel)) {
localChannels[channel] = mutableSetOf();
}
channels[channel]!!.add(node);
localChannels[channel]!!.add(node);
}
fun removeFromChannel(channel: String, node: Node) {
if(!channels.containsKey(channel)) return;
channels[channel]?.remove(node);
if(channels[channel].isNullOrEmpty()) {
channels.remove(channel);
val localChannels = channels.get()
if(!localChannels.containsKey(channel)) return;
localChannels[channel]?.remove(node);
if(localChannels[channel].isNullOrEmpty()) {
localChannels.remove(channel);
}
}
fun emitChannelMessage(starter: Node, channel: String, message: Message) {
val c = channels[channel] ?: return;
val localChannels = channels.get()
val c = localChannels[channel] ?: return;
c.forEach { if(it != starter) it.received(message); };
}
}

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() }
}
}

View File

@@ -28,6 +28,7 @@
"item.neocomputers.lan": "Wired Network Card",
"item.neocomputers.wlan0": "Wireless Network Card (Tier 1)",
"item.neocomputers.wlan1": "Wireless Network Card (Tier 2)",
"item.neocomputers.tunnel": "Linked Card",
"item.neocomputers.data0": "Data Card (Tier 1)",
"item.neocomputers.data1": "Data Card (Tier 2)",
"item.neocomputers.data2": "Data Card (Tier 3)",
@@ -47,5 +48,6 @@
"neocomputers.memory.capacity": "Capacity: %1$s",
"neocomputers.eeprom.codeused": "Code Storage: %1$s / %2$s",
"neocomputers.eeprom.dataused": "Data Storage: %1$s / %2$s",
"neocomputers.tunnel.channel": "Linked Channel: %1$s",
"sounds.neocomputers.computer_running": "Computer Fans"
}