Posted July 5, 20178 yr 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 July 5, 20178 yr by BenignBanana Fixed a bug that was added after testing.
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.