Jump to content

BenignBanana

Forge Modder
  • Posts

    49
  • Joined

  • Last visited

Converted

  • Gender
    Undisclosed
  • Personal Text
    I am new!

Recent Profile Visitors

The recent visitors block is disabled and is not being shown to other users.

BenignBanana's Achievements

Tree Puncher

Tree Puncher (2/8)

0

Reputation

  1. 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.
  2. Just posting one last update here for anyone who sees this in the future. Turns out there IS a way to rotate around the Z axis, it's just really hard to find. I stumbled upon it after googling for blockstate transforms (as in moving the model). See Forge Blockstate V1 specs for all the details. Do keep in mind that any Y rotation you may do in a facing variant will override these transforms.
  3. Yea I looked at the ModelRotation code. That's some of the most inflexible code I've ever seen. Guess I'll just have to bake it in then. Anyways, thanks for the help.
  4. Is there no way to do this other than baking the y rotation into a copy of the model?
  5. Yea this definitely doesn't work. After recreating the BlockState scenarios in Maya it seems that Y is always applied after X, making this trick impossible. Applying y 270 first, then x 90 gives me the correct result in Maya (as it should), but what I see in Minecraft is equivalent to applying x 90 first, then y 270. This is what I did in my JSON: "corner": { "model": "kaidencraft:boilertank/end.obj", "y": 270, "x": 90 },
  6. Yea I figured that out now with some hand moving and loading a test object into Maya But I'm still struggling to understand how that will interact with the facing property that I use for Y rotation.
  7. Can you tell me how to do that specifically? I'm terrible at 3D visualization and vector math. Also, would the y rotation work additively with the facing?
  8. Hey everyone, I tried my hand at some X and Z rotating to save myself a bunch of models on my connected model, but it seems "z": 90 doesn't to anything. "x":90 works just fine and so does "y":90, but z seems to be completely ignored. Why could this be and how can I fix it? Here's my BlockState (it's a long one): { "forge_marker": 1, "variants": { "normal": [ {"model": "kaidencraft:boilertank/single.obj"} ], "inventory": [ {} ], "mb_part": { "single": { "model": "kaidencraft:boilertank/single.obj" }, "end": { "model": "kaidencraft:boilertank/pillar_bottom.obj", "x": 90 }, "line_edge": { "model": "kaidencraft:boilertank/pillar_center.obj", "z": 90 }, "corner": { "model": "kaidencraft:boilertank/end.obj", "z": 90 }, "edge": { "model": "kaidencraft:boilertank/edge_single.obj" }, "center": { "model": "kaidencraft:boilertank/center_single.obj" }, "bottom_end": { "model": "kaidencraft:boilertank/end.obj" }, "bottom_edge": { "model": "kaidencraft:boilertank/edge.obj" }, "bottom_corner": { "model": "kaidencraft:boilertank/corner.obj" }, "bottom_center": { "model": "kaidencraft:boilertank/center_top.obj", "x": 180 }, "bottom_pillar": { "model": "kaidencraft:boilertank/pillar_bottom.obj" }, "center_end": { "model": "kaidencraft:boilertank/edge_single.obj", "x": 90, "z": 90 }, "center_corner": { "model": "kaidencraft:boilertank/edge.obj", "z": 90, "x": 90 }, "center_edge": { "model": "kaidencraft:boilertank/center_top.obj", "x": 270 }, "center_line_edge": { "model": "kaidencraft:boilertank/center_single.obj", "x": 90 }, "center_pillar": { "model": "kaidencraft:boilertank/pillar_center.obj" }, "top_end": { "model": "kaidencraft:boilertank/end.obj", "z": 180 }, "top_corner": { "model": "kaidencraft:boilertank/corner.obj", "x": 180 }, "top_edge": { "model": "kaidencraft:boilertank/edge.obj", "x": 90 }, "top_line_edge": { "model": "kaidencraft:boilertank/edge_single.obj", "x": 90 }, "top_pillar": { "model": "kaidencraft:boilertank/pillar_bottom.obj", "x": 180 }, "top_center": { "model": "kaidencraft:boilertank/center_top.obj" }, "hidden": { "model": "kaidencraft:blank.obj" } }, "facing": { "north": {}, "south": { "y": 180 }, "west": { "y": 270 }, "east": { "y": 90 } } }, "defaults": { "model": "kaidencraft:boilertank/single.obj", "textures": { "#initialShadingGroup": "kaidencraft:block/boiler_main" } } } The part I'm testing with is "corner", which rotates fine when I put in an x rotation, but does nothing when I put in various z rotations. Cheers in advance Edit for posterity: You can get any transforms you want using forge specific BlockState parts - see Forge Blockstate V1 specs for all details. Only the last specified transform will be applied - this includes simple rotation like you would use in a facing variant.
  9. I don't access the world or blocks when making the tag, I wanted to use my "own" (really OpenModsLib's) sync system to send the syncable info over a custom network channel. That code needs to get the TileEntity at the relevant position so it can handle the incoming sync data. I solved this for now by just serializing the sync map into NBT and returning that in getUpdateTag(), but I'd really prefer to use a unified sync method. The readFromNBT()/writeToNBT() on the sync map are only supposed to be called when loading/saving the world, so I dislike calling it for synchronisation. So I need an event that triggers after a player has loaded the world from the server. Edit: Here's my code that requires the world to be loaded: 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 }
  10. Hey everyone, after loads of debugging work I finally managed to make my multiblock with custom models work, but I noticed this weird graphical issue on two sides of my model. There is what looks like a lighting glitch, manifesting as a dark blob, spread out across the entire side of the multiblock, even between the separate block models. Here's some images: Not sure what could be causing this. The models are identical, just rotated 90/180 degrees. Any ideas? Edit: I'm, obviously talking about the bottom models. The top ones are just placeholders.
  11. Alright so I used getUpdateTag() which does actually fire (tried getUpdatePacket() yesterday, which doesn't seem to fire in SP). However it seems at the time getUpdateTag() is called, the world isn't properly loaded yet and all blocks return as air. Where do I need to hook in to get the first viable state where the player is watching and the client has already loaded the world from the server? Edit: I use a custom sync system with all the networking handled in my library, I could use writeToNBT() but it would significantly increase the overhead for networking.
  12. Hey everyone, I hate to ask for yet more help after all my threads over the last few days, but this is a framework question so at least I'm not just an idiot and have a bug in my own code. I need to do an initial full sync for my TileEntities whenever a player starts watching it (or in the case of the client, as soon as the single player gets loaded in). How can I hook into this to trigger the sync? I tried to put it in onLoad() but that seems to be called before the client is loaded and it wouldn't work for multiplayer anyways. Cheers in advance
  13. Nevermind, I guess it was a breakpoint not working properly. I had one in my getMultiblockPart() method which is called from getActualState() and that wasn't getting triggered, but after doing a full recompile and restart it's working fine. The issue that was causing it to not work was the fact that the TE wasn't serialized because I didn't properly exit and instead just stopped it in the debugger. It also seems to be called before the server/client sync so I'll have to recalculate the rendering in my sync event anyways. Derp.
  14. Hey everyone, I noticed that my additional blockstate data works fine when placing/breaking blocks, but the game doesn't call getActualState() on startup. Is there a "proper" way to get it called? I could always add a bit of one time code into the TileEntity update but that feels a bit hacky and roundabout. So does Forge/MC have an actual way to do this?
  15. Alright I finally figured out what was going on with the events not being called server side. Turns out in this new world, unlike before, the multiblock parts had different Y coordinates instead of different X coordinates, which caused an infinite loop in my rebuild function (due to a mistake of mine), but I couldn't catch this in step through because the logic was in a lambda delegate (which is executed in a separate context, hence no stepping). As a result the server got frozen and any further events were no longer sent. I thought the whole debugger hanging up was an issue because of spending too long in paused mode (which happens fairly frequently), but after a few restarts I noticed it always happened in the same spot, allowing me to spot the issue. I learned my lesson: Always replace delegates with regular for loops while debugging. The issue with TEs being replaced is also fixed thanks to jeffryfisher's help. The question about a discord or something still stands though, I'd much rather ask for help in a chat program if it's something that won't be useful to see for anyone else in the future.
×
×
  • Create New...

Important Information

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