diff --git a/src/main/kotlin/org/neoflock/neocomputers/NeoComputers.kt b/src/main/kotlin/org/neoflock/neocomputers/NeoComputers.kt index c5132b5..42b160f 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/NeoComputers.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/NeoComputers.kt @@ -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,13 +112,18 @@ object NeoComputers { NodeSynchronizer.playerScreenClosed(player) } - NetworkManager.registerReceiver(NetworkManager.c2s(),NodeSynchronizer.ScreenDataPayload.TYPE, NodeSynchronizer.ScreenDataPayload.CODEC, { - packet, ctx -> - val player = ctx.player - if(player is ServerPlayer) { - NodeSynchronizer.screenMap[player]?.processScreenInteraction(player, packet.buffer) - } - }) + // 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 + if(player is ServerPlayer) { + 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!") diff --git a/src/main/kotlin/org/neoflock/neocomputers/block/CaseBlock.kt b/src/main/kotlin/org/neoflock/neocomputers/block/CaseBlock.kt index ff04f79..1b64059 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/block/CaseBlock.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/block/CaseBlock.kt @@ -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) { builder.add(COMPUTER_RUNNING) diff --git a/src/main/kotlin/org/neoflock/neocomputers/block/NodeBlock.kt b/src/main/kotlin/org/neoflock/neocomputers/block/NodeBlock.kt index 5309df1..d27eddb 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/block/NodeBlock.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/block/NodeBlock.kt @@ -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(SCREEN_SYNC_ID) + val SCREEN_DATA_ID = ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "screen_data") + val TYPE = CustomPacketPayload.Type(SCREEN_DATA_ID) val CODEC = object : StreamCodec { 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(BEEP_DATA_ID) + val CODEC = object : StreamCodec { + 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() 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? { return object : BlockEntityTicker { 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) } } diff --git a/src/main/kotlin/org/neoflock/neocomputers/entity/BlockEntities.kt b/src/main/kotlin/org/neoflock/neocomputers/entity/BlockEntities.kt index c3b3255..d1b45eb 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/entity/BlockEntities.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/entity/BlockEntities.kt @@ -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()) } } \ No newline at end of file diff --git a/src/main/kotlin/org/neoflock/neocomputers/entity/CaseBlockEntity.kt b/src/main/kotlin/org/neoflock/neocomputers/entity/CaseBlockEntity.kt index 08d423d..6c08cd3 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/entity/CaseBlockEntity.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/entity/CaseBlockEntity.kt @@ -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 = NonNullList.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 = 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") + } + } + } + } } \ No newline at end of file diff --git a/src/main/kotlin/org/neoflock/neocomputers/entity/MachineEntity.kt b/src/main/kotlin/org/neoflock/neocomputers/entity/MachineEntity.kt index 3d2b1be..27acd6d 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/entity/MachineEntity.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/entity/MachineEntity.kt @@ -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 + fun setMachineArchitecture(arch: String) // Redstone signals fun getRedstoneInput(direction: Direction): Int diff --git a/src/main/kotlin/org/neoflock/neocomputers/gui/menu/CaseMenu.kt b/src/main/kotlin/org/neoflock/neocomputers/gui/menu/CaseMenu.kt index 8e7a674..e355b5a 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/gui/menu/CaseMenu.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/gui/menu/CaseMenu.kt @@ -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); diff --git a/src/main/kotlin/org/neoflock/neocomputers/gui/screen/CaseScreen.kt b/src/main/kotlin/org/neoflock/neocomputers/gui/screen/CaseScreen.kt index 22037cd..f608c14 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/gui/screen/CaseScreen.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/gui/screen/CaseScreen.kt @@ -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 { private val PCB: ResourceLocation = ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "textures/gui/computer.png") @@ -34,11 +32,49 @@ class CaseScreen : GenericContainerScreen { 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 { + 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 { 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(), 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) } } \ No newline at end of file diff --git a/src/main/kotlin/org/neoflock/neocomputers/item/CPUItem.kt b/src/main/kotlin/org/neoflock/neocomputers/item/CPUItem.kt index c6c70db..7ec8de4 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/item/CPUItem.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/item/CPUItem.kt @@ -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 = setOf("Lua 5.3") + override fun toComponentNode(itemStack: ItemStack, machine: MachineEntity): Networking.Node? = null } diff --git a/src/main/kotlin/org/neoflock/neocomputers/item/ComponentItem.kt b/src/main/kotlin/org/neoflock/neocomputers/item/ComponentItem.kt index b90bcc5..94c2126 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/item/ComponentItem.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/item/ComponentItem.kt @@ -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 = setOf() // Component placed, node must now exist fun whenComponentPlaced(itemStack: ItemStack, machine: MachineEntity, newRole: String) { diff --git a/src/main/kotlin/org/neoflock/neocomputers/item/DataComponents.kt b/src/main/kotlin/org/neoflock/neocomputers/item/DataComponents.kt index 0672974..ff3ac77 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/item/DataComponents.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/item/DataComponents.kt @@ -19,4 +19,11 @@ object DataComponents { DataComponentType.builder().persistent(Codec.BYTE_BUFFER).build()) val EEPROM_DATA = Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "eeprom_data"), DataComponentType.builder().persistent(Codec.BYTE_BUFFER).build()) + val EEPROM_CODESIZE = Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "eeprom_codesize"), + DataComponentType.builder().persistent(Codec.INT).build()) + val EEPROM_DATASIZE = Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "eeprom_datasize"), + DataComponentType.builder().persistent(Codec.INT).build()) + + val TUNNEL_CHANNEL = Registry.register(BuiltInRegistries.DATA_COMPONENT_TYPE, ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "tunnel_channel"), + DataComponentType.builder().persistent(Codec.STRING).build()) } \ No newline at end of file diff --git a/src/main/kotlin/org/neoflock/neocomputers/item/EEPROMItem.kt b/src/main/kotlin/org/neoflock/neocomputers/item/EEPROMItem.kt index f925967..2252887 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/item/EEPROMItem.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/item/EEPROMItem.kt @@ -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) diff --git a/src/main/kotlin/org/neoflock/neocomputers/item/Tabs.kt b/src/main/kotlin/org/neoflock/neocomputers/item/Tabs.kt index 1273835..16848a2 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/item/Tabs.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/item/Tabs.kt @@ -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) } diff --git a/src/main/kotlin/org/neoflock/neocomputers/item/TunnelCard.kt b/src/main/kotlin/org/neoflock/neocomputers/item/TunnelCard.kt index 2b07461..872c99a 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/item/TunnelCard.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/item/TunnelCard.kt @@ -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 = 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) diff --git a/src/main/kotlin/org/neoflock/neocomputers/network/Networking.kt b/src/main/kotlin/org/neoflock/neocomputers/network/Networking.kt index cddae74..867b278 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/network/Networking.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/network/Networking.kt @@ -246,8 +246,8 @@ object Networking { abstract fun receiveWireless(message: Message, emitter: WirelessEndpoint) } - val wirelessNodes = mutableSetOf() - val allNodes = mutableMapOf() + val wirelessNodes = ThreadLocal.withInitial { mutableSetOf() } + val allNodes = ThreadLocal.withInitial { mutableMapOf() } // 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>(); + val channels = ThreadLocal.withInitial { mutableMapOf>() } 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); }; } } \ No newline at end of file diff --git a/src/main/kotlin/org/neoflock/neocomputers/sounds/Sounds.kt b/src/main/kotlin/org/neoflock/neocomputers/sounds/Sounds.kt index 9cfedb5..99b8f4f 100644 --- a/src/main/kotlin/org/neoflock/neocomputers/sounds/Sounds.kt +++ b/src/main/kotlin/org/neoflock/neocomputers/sounds/Sounds.kt @@ -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() } + + 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.. 1) off -= 1f + finalBuf.put(value) + } + } + if(finalBuf.hasRemaining()) { + for(sample in 0..() + 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() } + } } \ No newline at end of file diff --git a/src/main/resources/assets/neocomputers/lang/en_us.json b/src/main/resources/assets/neocomputers/lang/en_us.json index 10246cb..294c0ff 100644 --- a/src/main/resources/assets/neocomputers/lang/en_us.json +++ b/src/main/resources/assets/neocomputers/lang/en_us.json @@ -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" } \ No newline at end of file