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.