synchronization and persistant state

This commit is contained in:
2026-04-18 11:53:18 +02:00
parent 834a9c7ed8
commit a4533e03bb
12 changed files with 320 additions and 98 deletions

View File

@@ -10,7 +10,7 @@ import org.neoflock.neocomputers.NeoComputers
import java.util.function.Supplier
import com.google.common.base.Suppliers
open class BaseBlock : Block(BlockBehaviour.Properties.of()) { // TODO: create a TieredBaseBlock class that extends this or something
open class BaseBlock(properties: Properties = Properties.of()) : Block(properties) { // TODO: create a TieredBaseBlock class that extends this or something
// val tier: Int
companion object Registry {

View File

@@ -1,6 +1,9 @@
package org.neoflock.neocomputers.block
import net.minecraft.client.player.LocalPlayer
import net.minecraft.core.BlockPos
import net.minecraft.core.HolderLookup
import net.minecraft.nbt.CompoundTag
import net.minecraft.network.chat.ChatType
import net.minecraft.network.chat.OutgoingChatMessage
import net.minecraft.network.chat.PlayerChatMessage
@@ -19,23 +22,18 @@ import org.neoflock.neocomputers.network.PowerRole
import kotlin.math.min
open class CapacitorEntity(val capacity: Long, type: BlockEntityType<*>, pos: BlockPos, state: BlockState) : NodeBlockEntity(type, pos, state) {
var amountStored: Long = 0
override val node = object : Networking.Node() {
override fun getPowerRole() = PowerRole.STORAGE
override fun getEnergy() = amountStored
override fun getEnergyCapacity() = capacity
override fun giveEnergy(amount: Long): Long {
val given = min(amount, capacity - amountStored)
amountStored += given
return given
}
override var powerRole = PowerRole.STORAGE
override var energyCapacity: Long = capacity
}
override fun withdrawEnergy(amount: Long): Long {
val taken = min(amount, amountStored)
amountStored -= taken
return taken
}
override fun loadAdditional(compoundTag: CompoundTag, provider: HolderLookup.Provider) {
node.energy = min(compoundTag.getLong("energy"), node.energyCapacity)
}
override fun saveAdditional(compoundTag: CompoundTag, provider: HolderLookup.Provider) {
compoundTag.putLong("energy", node.energy)
}
}
@@ -61,15 +59,15 @@ class CapacitorBlock(val tier: Int) : NodeBlock() {
player: Player,
blockHitResult: BlockHitResult
): InteractionResult {
if(!level.isClientSide()) {
val sp = player as ServerPlayer
if(level.isClientSide()) {
val p = player as LocalPlayer
val ent = level.getBlockEntity(blockPos)
if(ent is CapacitorEntity) {
if(sp.isCrouching) ent.amountStored++
val msg = PlayerChatMessage.system("energy: ${ent.amountStored} / ${ent.capacity} (${ent.computeEdges().size} edges, ${ent.node.getReachable().size} connected)")
sp.sendChatMessage(OutgoingChatMessage.create(msg), false, ChatType.bind(ChatType.CHAT, player))
if(p.isCrouching) ent.node.giveEnergy(1)
val msg = PlayerChatMessage.system("energy: ${ent.node.energy} / ${ent.capacity} (${ent.computeEdges().size} edges, ${ent.node.getReachable().size} connected)")
p.sendSystemMessage(OutgoingChatMessage.create(msg).content())
}
}
return InteractionResult.SUCCESS;
return InteractionResult.SUCCESS
}
}

View File

@@ -2,16 +2,30 @@ package org.neoflock.neocomputers.block
import dev.architectury.registry.menu.MenuRegistry
import net.minecraft.core.BlockPos
import net.minecraft.core.particles.ParticleTypes
import net.minecraft.network.chat.ChatType
import net.minecraft.network.chat.OutgoingChatMessage
import net.minecraft.network.chat.PlayerChatMessage
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.sounds.SoundEvent
import net.minecraft.sounds.SoundEvents
import net.minecraft.sounds.SoundSource
import net.minecraft.util.RandomSource
import net.minecraft.world.InteractionResult
import net.minecraft.world.entity.player.Player
import net.minecraft.world.item.ItemStack
import net.minecraft.world.level.BlockGetter
import net.minecraft.world.level.Level
import net.minecraft.world.level.block.Block
import net.minecraft.world.level.block.EntityBlock
import net.minecraft.world.level.block.FurnaceBlock
import net.minecraft.world.level.block.SoundType
import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.state.BlockState
import net.minecraft.world.level.block.state.StateDefinition
import net.minecraft.world.level.block.state.properties.BooleanProperty
import net.minecraft.world.level.storage.loot.LootParams
import net.minecraft.world.phys.BlockHitResult
import org.neoflock.neocomputers.entity.BlockEntities
import org.neoflock.neocomputers.entity.SolarGeneratorBlockEntity
@@ -22,7 +36,19 @@ class SolarGeneratorBlock : NodeBlock(), EntityBlock {
}
// TODO: make it glow when burning
class CombustionGeneratorBlock : NodeBlock(), EntityBlock {
class CombustionGeneratorBlock : NodeBlock, EntityBlock {
companion object {
val ACTIVE = BooleanProperty.create("active")
fun getLuminance(blockState: BlockState): Int {
return if(blockState.getValue(ACTIVE)) 5 else 0
}
}
constructor(): super(Properties.of().sound(SoundType.STONE).lightLevel(CombustionGeneratorBlock::getLuminance)) {
registerDefaultState(defaultBlockState().setValue(ACTIVE, false))
}
override fun newBlockEntity(blockPos: BlockPos, blockState: BlockState): BlockEntity = CombustionGeneratorBlockEntity(blockPos, blockState).initNetworking()
override fun useWithoutItem(
@@ -35,8 +61,25 @@ class CombustionGeneratorBlock : NodeBlock(), EntityBlock {
if(!level.isClientSide()) {
val sp = player as ServerPlayer
val ent = level.getBlockEntity(blockPos, BlockEntities.COMBUSTGEN_ENTITY.get()).get()
NodeSynchronizer.registerPlayerScreen(sp, ent)
MenuRegistry.openMenu(sp, ent)
}
return InteractionResult.SUCCESS
}
override fun createBlockStateDefinition(builder: StateDefinition.Builder<Block?, BlockState?>) {
builder.add(ACTIVE)
}
override fun animateTick(blockState: BlockState, level: Level, blockPos: BlockPos, randomSource: RandomSource) {
if(blockState.getValue(ACTIVE)) {
if(randomSource.nextDouble() < 0.1) level.playSound(null, blockPos, SoundEvents.FURNACE_FIRE_CRACKLE, SoundSource.AMBIENT)
val x = blockPos.x.toDouble()
val y = blockPos.y.toDouble()
val z = blockPos.z.toDouble()
level.addParticle(ParticleTypes.SMOKE, x+0.5, y+0.5, z+0.5, 0.0, 0.0, 0.0)
}
}
}

View File

@@ -1,6 +1,15 @@
package org.neoflock.neocomputers.block
import dev.architectury.networking.NetworkManager
import io.netty.buffer.Unpooled
import net.minecraft.core.BlockPos
import net.minecraft.network.FriendlyByteBuf
import net.minecraft.network.RegistryFriendlyByteBuf
import net.minecraft.network.codec.StreamCodec
import net.minecraft.network.protocol.common.custom.CustomPacketPayload
import net.minecraft.resources.ResourceLocation
import net.minecraft.server.level.ServerLevel
import net.minecraft.server.level.ServerPlayer
import net.minecraft.world.entity.LivingEntity
import net.minecraft.world.item.ItemStack
@@ -11,8 +20,73 @@ import net.minecraft.world.level.block.entity.BlockEntity
import net.minecraft.world.level.block.entity.BlockEntityTicker
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
object NodeSynchronizer {
class StatePayload(var blockPos: BlockPos, var buffer: FriendlyByteBuf): CustomPacketPayload {
companion object {
val NODE_SYNC_ID = ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "node_sync")
val TYPE = CustomPacketPayload.Type<StatePayload>(NODE_SYNC_ID)
val CODEC = object : StreamCodec<RegistryFriendlyByteBuf, StatePayload> {
override fun decode(buf: RegistryFriendlyByteBuf): StatePayload {
val blockPos = buf.readBlockPos()
val buffer = FriendlyByteBuf(buf.copy(buf.readerIndex(), buf.readableBytes()))
return StatePayload(blockPos, buffer)
}
override fun encode(buf: RegistryFriendlyByteBuf, payload: StatePayload) {
buf.writeBlockPos(payload.blockPos)
buf.writeBytes(payload.buffer)
}
}
}
override fun type() = TYPE
}
class ScreenPayload(var entityTypeWireID: String, var buffer: FriendlyByteBuf): CustomPacketPayload {
companion object {
val SCREEN_SYNC_ID = ResourceLocation.fromNamespaceAndPath(NeoComputers.MODID, "screen_sync")
val TYPE = CustomPacketPayload.Type<ScreenPayload>(SCREEN_SYNC_ID)
val CODEC = object : StreamCodec<RegistryFriendlyByteBuf, ScreenPayload> {
override fun decode(buf: RegistryFriendlyByteBuf): ScreenPayload {
val id = buf.readByteArray().decodeToString()
val buffer = FriendlyByteBuf(buf.copy(buf.readerIndex(), buf.readableBytes()))
return ScreenPayload(id, buffer)
}
override fun encode(buf: RegistryFriendlyByteBuf, payload: ScreenPayload) {
buf.writeByteArray(payload.entityTypeWireID.encodeToByteArray())
buf.writeBytes(payload.buffer)
}
}
}
override fun type() = TYPE
}
val screenMap = mutableMapOf<ServerPlayer, NodeBlockEntity>()
fun playerScreenClosed(player: ServerPlayer) {
screenMap.remove(player)
}
fun nodeTypeToWireID(nodeType: BlockEntityType<*>): String = nodeType.javaClass.canonicalName
fun registerPlayerScreen(player: ServerPlayer, entity: NodeBlockEntity) {
screenMap[player] = entity
}
fun syncScreens() {
for((player, ent) in screenMap) {
val buf = FriendlyByteBuf(Unpooled.buffer())
ent.encodeScreenData(player, buf)
NetworkManager.sendToPlayer(player, ScreenPayload(nodeTypeToWireID(ent.type), buf))
}
}
}
abstract class NodeBlockEntity(blockEntityType: BlockEntityType<*>, blockPos: BlockPos, blockState: BlockState) : BlockEntity(blockEntityType, blockPos, blockState) {
abstract val node: Networking.Node
@@ -21,6 +95,27 @@ abstract class NodeBlockEntity(blockEntityType: BlockEntityType<*>, blockPos: Bl
return this
}
// runs on the server, meant to encode state to send to all players
open fun encodeDownstreamData(packet: FriendlyByteBuf) {
packet.writeUUID(node.address)
packet.writeLong(node.energy)
packet.writeLong(node.energyCapacity)
packet.writeEnum(node.reachability)
packet.writeEnum(node.powerRole)
}
// runs on the client, meant to decode server state packets to synchronize client state
open fun syncWithUpstream(packet: FriendlyByteBuf) {
node.address = packet.readUUID()
node.energy = packet.readLong()
node.energyCapacity = packet.readLong()
node.reachability = packet.readEnum(node.reachability.javaClass)
node.powerRole = packet.readEnum(node.powerRole.javaClass)
}
// Encodes data meant for the associated screen of a player
open fun encodeScreenData(player: ServerPlayer, packet: FriendlyByteBuf) {}
private var stateIsDirty = true
open fun getNeighbourEntities(): List<BlockEntity> {
@@ -36,7 +131,7 @@ abstract class NodeBlockEntity(blockEntityType: BlockEntityType<*>, blockPos: Bl
return subpos.mapNotNull { pos -> level?.getBlockEntity(pos) }
}
fun computeEdges(): Set<NodeBlockEntity> {
open fun computeEdges(): Set<NodeBlockEntity> {
val s = mutableSetOf<NodeBlockEntity>()
val neighbours = getNeighbourEntities()
for(neighbour in neighbours) {
@@ -47,13 +142,21 @@ abstract class NodeBlockEntity(blockEntityType: BlockEntityType<*>, blockPos: Bl
return s
}
fun invalidateNodeState() {
open fun invalidateNodeState() {
stateIsDirty = true
}
fun needsSynchronization() = stateIsDirty
open fun tickNode() {
open fun tickNode(level: Level) {
if(!level.isClientSide) {
val l = level as ServerLevel
val packetBuf = FriendlyByteBuf(Unpooled.buffer())
encodeDownstreamData(packetBuf)
l.players().forEach {
if(it.level().isLoaded(blockPos)) NetworkManager.sendToPlayer(it, NodeSynchronizer.StatePayload(blockPos, packetBuf))
}
}
if(!stateIsDirty) return
stateIsDirty = false
computeEdges().forEach {
@@ -73,7 +176,7 @@ abstract class NodeBlockEntity(blockEntityType: BlockEntityType<*>, blockPos: Bl
}
}
abstract class NodeBlock: BaseBlock(), EntityBlock {
abstract class NodeBlock(properties: Properties = Properties.of()): BaseBlock(properties), EntityBlock {
override fun <T : BlockEntity> getTicker(
level: Level,
blockState: BlockState,
@@ -82,7 +185,7 @@ abstract class NodeBlock: BaseBlock(), EntityBlock {
return object : BlockEntityTicker<T> {
override fun tick(level: Level, blockPos: BlockPos, blockState: BlockState, blockEntity: T) {
if(blockEntity !is NodeBlockEntity) return;
blockEntity.tickNode()
blockEntity.tickNode(level)
}
}
}