Jump to content

Custom Entity not being tracked by client


BenignBanana

Recommended Posts

Hey everyone,

all the info I could google on custom entities pertains to living entities - but I need to use a straight extension of Entity for my multiblock structures. I managed to get it all working just fine, except the entity doesn't get sent out to the client. The client is clearly in range (I set the tracking range to 32, with the player being within at most 5 blocks of the entity). The entities position is correct on the server, and the register method gets called just fine too. Here's all the code (nevermind the registration in EntityBoilerMultiblock, a breakpoint there does indeed trigger so the way I call it is irrelevant as far as this is concerned). I also annotated stuff that might be confusing to people unfamiliar with Kotlin, the rest should be pretty much self explanatory.

 

The SyncedEntity class:

// ?. = null safe version of . | ?: fallback if null

abstract class SyncedEntity(world: World) : Entity(world), ISyncMapProvider, IEntityAdditionalSpawnData {

    override val syncMap: SyncMapEntity<SyncedEntity> = SyncMapEntity(this)

    init { //constructor
        createSyncedFields()
        SyncObjectScanner.instance.registerAllFields(syncMap, this)
    }

    override fun writeEntityToNBT(compound: NBTTagCompound) {
        super.writeToNBT(compound)
        syncMap.writeToNBT(compound)
    }

    override fun readEntityFromNBT(compound: NBTTagCompound) {
        super.readFromNBT(compound)
        syncMap.readFromNBT(compound)
    }

    protected abstract fun createSyncedFields()
    fun addSyncedObject(name: String, obj: ISyncableObject) { syncMap.put(name, obj) }
    fun sync() = syncMap.sync()

    override fun readSpawnData(additionalData: ByteBuf) {
        syncMap.readFromStream(DataInputStream(ByteBufInputStream(additionalData)))
    }

    override fun writeSpawnData(buffer: ByteBuf) {
        syncMap.writeToStream(DataOutputStream(ByteBufOutputStream(buffer)), true)
    }

    protected fun createPacket(fullPacket: Boolean): Packet<*> {
        val payload = syncMap.createPayload(fullPacket)
        return SyncChannelHolder.createPacket(payload)
    }

    override fun entityInit() {

    }

    override fun getEntityData(): NBTTagCompound {
        val tag = NBTTagCompound()
        syncMap.writeToNBT(NBTTagCompound())
        return tag
    }
}

 

And the EntityMultiblock class:

abstract class EntityMultiblock(world: World) : SyncedEntity(world) {
    abstract fun destroy()
    override fun canRenderOnFire(): Boolean = false
    override fun canBeCollidedWith(): Boolean = false
    override fun canBePushed(): Boolean = false
    override fun dealFireDamage(amount: Int) = Unit
    override fun attackEntityFrom(source: DamageSource?, amount: Float): Boolean = false
    override fun canTrample(world: World?, block: Block?, pos: BlockPos?, fallDistance: Float): Boolean = false
}

 

And finally the EntityBoilerMultiblock class (with some unrelated parts removed for brevity):

class EntityBoilerMultiblock(world: World) : EntityMultiblock(world), IHasGui, IInventoryProvider, IItemHandler {
    companion object { //static
        init {
            KaidenCraft.preInitEvent += { EntityRegistry.registerModEntity(ResourceLocation(KaidenCraft.MOD_ID, "multiblock_boiler"),
                    EntityBoilerMultiblock::class.java, "multiblock_boiler", 0, KaidenCraft.instance, 32, 20, false) } //lambda added to handler list
        }

        fun rebuild(world: World, pos: BlockPos): EntityBoilerMultiblock? {
			...			

            val entity = EntityBoilerMultiblock(world)
            entity.setPosition(currentPos.x.toDouble(), currentPos.y.toDouble(), currentPos.z.toDouble())
            boilerBlocks.join(tankBlocks).forEach { // exactly the same as for(BlockPos it : tankBlocks)
                val te = world.getTileEntity(it)
                if(te is TileEntityMultiblockPart) {
                    if(te.multiblockId.value != 0) getMultiblockEnt(world, te)?.destroy()
                    te.multiblockId.value = entity.entityId
                }
            }

            entity.boilerBlocks.addAll(boilerBlocks)
            entity.tankBlocks.addAll(tankBlocks)
            Log.debug("Found multiblock, contains ${boilerBlocks.size} boiler/tank units")
            return entity

			//code in TileEntityMultiblockPart: if(entity != null) world.spawnEntity(entity)
        }

        private fun getMultiblockEnt(world: World, te: TileEntityMultiblockPart): EntityMultiblock? {
            return (world.getEntityByID(te.multiblockId.value)) as? EntityMultiblock
        }
    }

    override fun destroy() {
        ...
    }

    lateinit var boilerBurnTime: SyncableInt
    lateinit var currentItemBurnTime: SyncableInt
    lateinit var waterTank: SyncableTank
    lateinit var steamTank: SyncableTank
    lateinit var boilerBlocks: SyncableCoordList
    lateinit var tankBlocks: SyncableCoordList

    override fun onEntityUpdate() {
        ...
    }

    override fun insertItem(slot: Int, stack: ItemStack, simulate: Boolean): ItemStack {
        
    }

    override fun getStackInSlot(slot: Int): ItemStack = inventory.getStackInSlot(slot)
    override fun getSlotLimit(slot: Int): Int = inventory.sizeInventory
    override fun getSlots(): Int = 1
    override fun extractItem(slot: Int, amount: Int, simulate: Boolean): ItemStack {
        return emptyItemStack
    }

    override fun createSyncedFields() {
        syncMap.sentSyncEvent += {
            if(it.changes.contains(waterTank)) waterTank.capacity = 4000 * tankBlocks.size
            if(it.changes.contains(steamTank)) steamTank.capacity = 4000 * tankBlocks.size
        }

        boilerBurnTime = SyncableInt()
        currentItemBurnTime = SyncableInt()
        boilerBlocks = SyncableCoordList()
        tankBlocks = SyncableCoordList()
        waterTank = SyncableTank(4000)
        steamTank = SyncableTank(4000)
    }

    override fun writeToNBT(tag: NBTTagCompound): NBTTagCompound {
        super.writeToNBT(tag)
        tag.setTag("inventory", inventory.serializeNBT())
        return tag
    }

    override fun readFromNBT(tag: NBTTagCompound) {
        super.readFromNBT(tag)
        inventory.deserializeNBT(tag.getCompoundTag("inventory"))
    }

    override val inventory: InventorySerializable = object: InventorySerializable("boiler", false, 1) {
        override fun isItemValidForSlot(i: Int, stack: ItemStack): Boolean = GameRegistry.getFuelValue(stack) > 0
    }

    override fun getServerGui(player: EntityPlayer): Any {
        return ContainerBoiler(player.inventory, this)
    }

    override fun getClientGui(player: EntityPlayer): Any {
        return GuiBoiler(ContainerBoiler(player.inventory, this))
    }

    override fun canOpenGui(player: EntityPlayer): Boolean = true
    fun isFurnaceBurning(): Boolean = boilerBurnTime.value > 0
}

 

Also the SyncMap class since this is about syncing:

class SyncMapEntity<out H>(handler: H) : SyncMap<H>(handler) where H : Entity, H : ISyncMapProvider {

    override val handlerType: SyncMap.HandlerType = HandlerType.ENTITY

    override val playersWatching: Set<EntityPlayerMP> by lazy { NetUtils.getPlayersWatchingEntity(handler.world as WorldServer, handler.entityId) } // just lazy loading the value

    override val world: World = handler.world

    override val invalid = handler.isDead
}

abstract class SyncMap<out H : ISyncMapProvider>(protected val handler: H) {
    companion object { // static
        class SyncFieldException : RuntimeException {
            constructor(cause: Throwable, name: String) : super("Failed to sync field '$name'", cause)
            constructor(cause: Throwable, index: Int) : super("Failed to sync field $index", cause)
        }

        private val MAX_OBJECT_NUM = 16

        @Throws(IOException::class)
        fun findSyncMap(world: World, input: DataInput): ISyncMapProvider? {
            val handlerTypeId = ByteUtils.readVLI(input)

            require(handlerTypeId < HandlerType.TYPES.size) { "SERIOUS BUG!!! handler type" }

            val handlerType = HandlerType.TYPES[handlerTypeId]
            val handler = handlerType.findHandler(world, input)
            return handler
        }
    }

    class SyncEvent(val changes: Set<ISyncableObject>)

    abstract class HandlerType {
        companion object { //static
            val ENTITY = object: HandlerType() {
                override val ordinal = 1

                override fun findHandler(world: World, input: DataInput): ISyncMapProvider? {
                    val entityId = input.readInt()
                    val entity = world.getEntityByID(entityId)
                    if(entity is ISyncMapProvider) return entity

                    Log.warn("Invalid handler info: can't find ISyncHandler entity id $entityId")
                    return null
                }

                override fun writeHandlerInfo(handler: ISyncMapProvider, output: DataOutput) {
                    try {
                        val e = handler as Entity
                        output.writeInt(e.entityId)
                    } catch (e: ClassCastException) {
                        throw RuntimeException("Invalid usage of handler type", e)
                    }
                }
            }

            val TILE_ENTITY = object: HandlerType() {
                override val ordinal = 0

                override fun findHandler(world: World, input: DataInput): ISyncMapProvider? {
                    val x = input.readInt()
                    val y = input.readInt()
                    val z = input.readInt()
                    val blockPos = BlockPos(x, y ,z)

                    if(!world.isAirBlock(blockPos)) {
                        val tile = world.getTileEntity(blockPos)
                        if(tile is ISyncMapProvider) return tile
                    }

                    Log.warn("Invalid handler info: can't find ISyncHandler TE @ ($x,$y,$z)", x, y, z)
                    return null
                }

                override fun writeHandlerInfo(handler: ISyncMapProvider, output: DataOutput) {
                    try {
                        val tileEntity = handler as TileEntity
                        output.writeInt(tileEntity.pos.x)
                        output.writeInt(tileEntity.pos.y)
                        output.writeInt(tileEntity.pos.z)
                    } catch (e: ClassCastException) {
                        throw RuntimeException("Invalid usage of handler type", e)
                    }
                }
            }

            internal val TYPES = arrayOf(TILE_ENTITY, ENTITY)
        }

        @Throws(IOException::class)
        abstract fun findHandler(world: World, input: DataInput): ISyncMapProvider?

        @Throws(IOException::class)
        abstract fun writeHandlerInfo(handler: ISyncMapProvider, output: DataOutput)
        abstract val ordinal: Int
    }

    private val knownUsers = hashSetOf<Int>()

    private val objects = arrayOfNulls<ISyncableObject>(16)
    private val nameMap = hashMapOf<String, ISyncableObject>()
    private val objectToId = Maps.newIdentityHashMap<ISyncableObject, Int>()

    val sentSyncEvent = Event<SyncEvent>()
    val receivedSyncEvent = Event<SyncEvent>()
    val clientInitEvent = Event<SyncEvent>()

    private var index = 0

    val size get() = index

    fun put(name: String, value: ISyncableObject) {
        require(index < MAX_OBJECT_NUM) { "Can't add more than $MAX_OBJECT_NUM objects" }
        val objId = index++
        objects[objId] = value //equivalent to objects.put(objId, value)
        nameMap[name] = value
        val prev = objectToId.put(value, objId)
        require(prev == null) { "Object $value registered twice, under ids $prev and $objId" }
    }

    operator fun set(name: String, value: ISyncableObject) = put(name, value)

    operator fun get(name: String) = nameMap["name"] ?: throw NoSuchElementException(name)
    operator fun get(objectId: Int): ISyncableObject {
        try {
            return objects[objectId] ?: throw NoSuchElementException(objectId.toString())
        } catch (e: ArrayIndexOutOfBoundsException) {
            throw NoSuchElementException(objectId.toString())
        }
    }

    fun getId(objectt: ISyncableObject): Int {
        return objectToId[objectt] ?: throw NoSuchElementException(objectt.toString())
    }

    @Throws(IOException::class)
    fun readFromStream(dataInputStream: DataInputStream) {
        var mask = dataInputStream.readShort()
        val changes = Sets.newIdentityHashSet<ISyncableObject>()
        var currentBit = 0

        while(mask != 0.toShort()) {
            if((mask and 1.toShort()) != 0.toShort()) {
                val objectt = objects[currentBit]
                if(objectt != null) {
                    try {
                        objectt.readFromStream(dataInputStream)
                    } catch (e: Throwable) {
                        throw SyncFieldException(e, currentBit)
                    }
                    changes.add(objectt)
                }
            }
            currentBit++
            mask = (mask.toInt() shr 1).toShort()
        }

        if(!changes.isEmpty()) receivedSyncEvent.fire(SyncEvent(Collections.unmodifiableSet(changes)))
    }

    @Throws(IOException::class) fun writeToStream(dataOutputStream: DataOutputStream, fullPacket: Boolean) {
        var mask = 0
        for(i in 0..index-1) {
            val objectt = objects[i]
            if(objectt != null && (fullPacket || objectt.isDirty())) {
                mask = ByteUtils.on(mask, i)
            }
        }
        dataOutputStream.writeShort(mask)

        for(i in 0..index-1) {
            val objectt = objects[i]
            if(objectt != null && (fullPacket || objectt.isDirty())) {
                try {
                    objectt.writeToStream(dataOutputStream)
                } catch (t: Throwable) {
                    throw SyncFieldException(t, i)
                }
            }
        }
    }

    protected abstract val handlerType: HandlerType
    protected abstract val playersWatching: Set<EntityPlayerMP>
    protected abstract val world: World
    protected abstract val invalid: Boolean

    fun sync() {
        require(!world.isRemote) { "This method can only be used server side" }
        if(invalid) return

        val changes = listChanges()
        val hasChanges = !changes.isEmpty()

        val fullPacketTargets = arrayListOf<EntityPlayerMP>()
        val deltaPacketTargets = arrayListOf<EntityPlayerMP>()

        val players = playersWatching
        for(player in players) {
            if(knownUsers.contains(player.entityId)) {
                if(hasChanges) deltaPacketTargets.add(player)
            } else {
                knownUsers.add(player.entityId)
                fullPacketTargets.add(player)
            }
        }

        try {
            if(!deltaPacketTargets.isEmpty()) {
                val deltaPayload = createPayload(false)
                SyncChannelHolder.instance.value.sendPayloadToPlayers(deltaPayload, deltaPacketTargets)
            }
        } catch (e: IOException) {
            Log.warn(e, "IOError during delta sync")
        }

        try {
            if(!fullPacketTargets.isEmpty()) {
                val fullPayload = createPayload(true)
                SyncChannelHolder.instance.value.sendPayloadToPlayers(fullPayload, fullPacketTargets)
            }
        } catch (e: IOException) {
            Log.warn(e, "IOError during full sync")
        }

        if(hasChanges) {
            markClean(changes)
            sentSyncEvent.fire(SyncEvent(Collections.unmodifiableSet(changes)))
        }
    }

    @Suppress("UNCHECKED_CAST")
    private fun listChanges() = objects.filter { it != null && it.isDirty() }.toSet() as Set<ISyncableObject>
    private fun markClean(changes: Set<ISyncableObject>) = changes.forEach { it.markClean() }

    @Throws(IOException::class)
    fun createPayload(fullPacket: Boolean): PacketBuffer {
        val output = Unpooled.buffer()
        val type = handlerType
        ByteBufUtils.writeVarInt(output, type.ordinal, 5)

        val dataOutputStream = DataOutputStream(ByteBufOutputStream(output))
        type.writeHandlerInfo(handler, dataOutputStream)
        writeToStream(dataOutputStream, fullPacket)

        return PacketBuffer(output.copy())
    }

    fun writeToNBT(tag: NBTTagCompound) {
        for((name, obj) in nameMap.entries) {
            try {
                obj.writeToNBT(tag, name)
            } catch (e: Throwable) {
                throw SyncFieldException(e, name)
            }
        }
    }

    fun readFromNBT(tag: NBTTagCompound) {
        for((name, obj) in nameMap.entries) {
            try {
                obj.readFromNBT(tag, name)
            } catch (e: Throwable) {
                throw SyncFieldException(e, name)
            }
        }
    }
}

Full GitHub for library and mod here and here

Hope someone can help me out here. Cheers in advance.

Edited by BenignBanana
Fixed a bug that was added after testing.
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Unfortunately, your content contains terms that we do not allow. Please edit your content to remove the highlighted words below.
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Announcements



×
×
  • Create New...

Important Information

By using this site, you agree to our Terms of Use.