BlameTaw Posted May 12, 2017 Posted May 12, 2017 Hello All, What is necessary to make a Tile Entity spawn stacks in the world from a GUI button press? I can make it work in the client side only, so I get phantom stacks in the world, and the inventory slot for the TileEntity refills to what it was if the GUI is closed and reopened. How do I make it happen server-side, too? Is there a standard way of communicating client-side changes to server-side? Quote
Choonster Posted May 12, 2017 Posted May 12, 2017 Use the Simple Network Implementation to send a packet to the server telling it that the player clicked the button. In the packet handler, check that the player has permission to spawn the stacks (e.g. is within interaction range of the block) before spawning them. Quote Please don't PM me to ask for help. Asking your question in a public thread preserves it for people who are having the same problem in the future.
BlameTaw Posted May 12, 2017 Author Posted May 12, 2017 (edited) Thanks, this looks like what I need. Do I need to make the changes on the client side, too, in this case? Like, when they click the button should I be modifying the stack amounts client-side AND server-side, and then spawning the stacks server-side only? (I assume spawning client and server at the same time would cause one phantom stack and one real stack.) Edited May 12, 2017 by BlameTaw Quote
Choonster Posted May 12, 2017 Posted May 12, 2017 On 5/12/2017 at 11:02 AM, BlameTaw said: Thanks, this looks like what I need. Do I need to make the changes on the client side, too, in this case? Like, when they click the button should I be modifying the stack amounts client-side AND server-side, and then spawning the stacks server-side only? (I assume spawning client and server at the same time would cause one phantom stack and one real stack.) Expand Just modify it on the server, the changes will be synced to the necessary clients. Quote Please don't PM me to ask for help. Asking your question in a public thread preserves it for people who are having the same problem in the future.
BlameTaw Posted May 12, 2017 Author Posted May 12, 2017 Is there a way to make it only happen when the GUI is closed, rather than when the button is pressed? I'm realizing that might make for a better user experience with this. If not, I'll try making it work on button press. Quote
V0idWa1k3r Posted May 12, 2017 Posted May 12, 2017 If your GUI extends GuiScreen or one of it's child classes there is a onGuiClosed() method you can override and send your packet from there. Quote
BlameTaw Posted May 12, 2017 Author Posted May 12, 2017 Oh wow I can't believe I missed that when I was looking at the other classes. Thanks! For the message itself, is there a common way people structure them? Is there a reason not to just convert a string to a byte array, or is it a better practice to just send relevant values and decode them on the other side? Quote
V0idWa1k3r Posted May 12, 2017 Posted May 12, 2017 A string will ultimately get converted to byte array anyway in the end, as will everything else you send. The only reason I see to not convert it manually is a chance that the charset can be messed up. For example if you are sending to the server a string client has typed and it contains unicode characters. If you are converting the arriving byte array to a string manually on the server you might use say, ASCII charset and mess that string up completely. There is a ByteBufUtils class that handles that for you though. Apart from that I think that structuring your packets is just a personal preference. If your packets are intended to be sent very frequiently and contain not much data it is the best to just write the data 'raw'. If your packets are big and contain bunch of data, especially if that data can vary (so you never know which parts the server/client will recieve) it might just be best to structure your data into an NBTTagCompound and just write/read that. Obviously sending only relevant data that has changed and needs to be synced is the best practice Quote
BlameTaw Posted May 12, 2017 Author Posted May 12, 2017 Ah that makes sense. I didn't think about character encodings. Either way I'm only sending a BlockPos so it's easy to just send three ints. I'm having an InstantiationException when I close the GUI, however. Seems to be coming from a NoSuchMethodException on my BufferEjectMessage.<init>() implying an issue during construction, maybe? I don't know why that would be. (stacktrace in the spoiler below the code) Here is my IMessage implemention and IMessageHandler implementation: package com.blametaw.itembuffers.blocks; import io.netty.buffer.ByteBuf; import net.minecraft.client.Minecraft; import net.minecraft.util.math.BlockPos; import net.minecraft.world.WorldServer; import net.minecraftforge.fml.common.network.simpleimpl.IMessage; import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler; import net.minecraftforge.fml.common.network.simpleimpl.MessageContext; import net.minecraftforge.items.CapabilityItemHandler; public class BufferEjectMessage implements IMessage { public BlockPos pos; //TileEntity Block Coordinates public BufferEjectMessage(int x, int y, int z){ this.pos = new BlockPos(x,y,z); } public BufferEjectMessage(BlockPos pos){ this.pos = pos; } @Override public void fromBytes(ByteBuf buf) { this.pos = new BlockPos(buf.readInt(), buf.readInt(), buf.readInt()); } @Override public void toBytes(ByteBuf buf) { buf.writeInt(pos.getX()); buf.writeInt(pos.getY()); buf.writeInt(pos.getZ()); } public static class MyMessageHandler implements IMessageHandler<BufferEjectMessage, IMessage> { @Override public IMessage onMessage(BufferEjectMessage message, MessageContext ctx) { //I assume this is wrong because I'd be greatly surprised if I was allowed to access game objects from within messages like this... TileEntityBasicBuffer te = (TileEntityBasicBuffer) Minecraft.getMinecraft().world.getTileEntity(message.pos); BufferStackHandler handler = (BufferStackHandler)te.getCapability(CapabilityItemHandler.ITEM_HANDLER_CAPABILITY, null); handler.ejectExcessItems(); return null; } } } Error stacktrace: Reveal hidden contents [15:49:57] [Netty Server IO #1/ERROR] [FML]: SimpleChannelHandlerWrapper exception io.netty.handler.codec.DecoderException: java.lang.InstantiationException: com.blametaw.itembuffers.blocks.BufferEjectMessage at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:99) ~[MessageToMessageDecoder.class:4.0.23.Final] at io.netty.handler.codec.MessageToMessageCodec.channelRead(MessageToMessageCodec.java:111) ~[MessageToMessageCodec.class:4.0.23.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:333) [AbstractChannelHandlerContext.class:4.0.23.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:319) [AbstractChannelHandlerContext.class:4.0.23.Final] at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:787) [DefaultChannelPipeline.class:4.0.23.Final] at io.netty.channel.embedded.EmbeddedChannel.writeInbound(EmbeddedChannel.java:169) [EmbeddedChannel.class:4.0.23.Final] at net.minecraftforge.fml.common.network.internal.FMLProxyPacket.processPacket(FMLProxyPacket.java:111) [FMLProxyPacket.class:?] at net.minecraft.network.NetworkManager.channelRead0(NetworkManager.java:157) [NetworkManager.class:?] at net.minecraft.network.NetworkManager.channelRead0(NetworkManager.java:51) [NetworkManager.class:?] at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:105) [SimpleChannelInboundHandler.class:4.0.23.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:333) [AbstractChannelHandlerContext.class:4.0.23.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:319) [AbstractChannelHandlerContext.class:4.0.23.Final] at net.minecraftforge.fml.common.network.handshake.NetworkDispatcher.handleServerSideCustomPacket(NetworkDispatcher.java:452) [NetworkDispatcher.class:?] at net.minecraftforge.fml.common.network.handshake.NetworkDispatcher.channelRead0(NetworkDispatcher.java:274) [NetworkDispatcher.class:?] at net.minecraftforge.fml.common.network.handshake.NetworkDispatcher.channelRead0(NetworkDispatcher.java:73) [NetworkDispatcher.class:?] at io.netty.channel.SimpleChannelInboundHandler.channelRead(SimpleChannelInboundHandler.java:105) [SimpleChannelInboundHandler.class:4.0.23.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:333) [AbstractChannelHandlerContext.class:4.0.23.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:319) [AbstractChannelHandlerContext.class:4.0.23.Final] at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:787) [DefaultChannelPipeline.class:4.0.23.Final] at io.netty.channel.local.LocalChannel.finishPeerRead(LocalChannel.java:326) [LocalChannel.class:4.0.23.Final] at io.netty.channel.local.LocalChannel.access$400(LocalChannel.java:45) [LocalChannel.class:4.0.23.Final] at io.netty.channel.local.LocalChannel$5.run(LocalChannel.java:312) [LocalChannel$5.class:4.0.23.Final] at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:380) [SingleThreadEventExecutor.class:4.0.23.Final] at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:357) [NioEventLoop.class:4.0.23.Final] at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:116) [SingleThreadEventExecutor$2.class:4.0.23.Final] at java.lang.Thread.run(Unknown Source) [?:1.8.0_131] Caused by: java.lang.InstantiationException: com.blametaw.itembuffers.blocks.BufferEjectMessage at java.lang.Class.newInstance(Unknown Source) ~[?:1.8.0_131] at net.minecraftforge.fml.common.network.FMLIndexedMessageToMessageCodec.decode(FMLIndexedMessageToMessageCodec.java:101) ~[FMLIndexedMessageToMessageCodec.class:?] at net.minecraftforge.fml.common.network.FMLIndexedMessageToMessageCodec.decode(FMLIndexedMessageToMessageCodec.java:40) ~[FMLIndexedMessageToMessageCodec.class:?] at io.netty.handler.codec.MessageToMessageCodec$2.decode(MessageToMessageCodec.java:81) ~[MessageToMessageCodec$2.class:4.0.23.Final] at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:89) ~[MessageToMessageDecoder.class:4.0.23.Final] ... 25 more Caused by: java.lang.NoSuchMethodException: com.blametaw.itembuffers.blocks.BufferEjectMessage.<init>() at java.lang.Class.getConstructor0(Unknown Source) ~[?:1.8.0_131] at java.lang.Class.newInstance(Unknown Source) ~[?:1.8.0_131] at net.minecraftforge.fml.common.network.FMLIndexedMessageToMessageCodec.decode(FMLIndexedMessageToMessageCodec.java:101) ~[FMLIndexedMessageToMessageCodec.class:?] at net.minecraftforge.fml.common.network.FMLIndexedMessageToMessageCodec.decode(FMLIndexedMessageToMessageCodec.java:40) ~[FMLIndexedMessageToMessageCodec.class:?] at io.netty.handler.codec.MessageToMessageCodec$2.decode(MessageToMessageCodec.java:81) ~[MessageToMessageCodec$2.class:4.0.23.Final] at io.netty.handler.codec.MessageToMessageDecoder.channelRead(MessageToMessageDecoder.java:89) ~[MessageToMessageDecoder.class:4.0.23.Final] ... 25 more Quote
BlameTaw Posted May 12, 2017 Author Posted May 12, 2017 (edited) I actually just figured it out. I didn't have a default constructor on the BufferEjectMessage. It is ejecting items now, however they are still phantom items. Is my message not actually executing server side, even though I'm calling sendToServer: Reference.networkWrapperInstance.sendToServer(new BufferEjectMessage(container.getTileEntity().getPos())); Does Minecraft.getMinecraft() not work for server-side? Is there another way I should be referencing the tile entity itself to call the eject functions? Also, is my assumption correct that I shouldn't be directly referencing the TileEntity from the message? That just seems like a threading nightmare waiting to happen... Edited May 12, 2017 by BlameTaw Quote
V0idWa1k3r Posted May 12, 2017 Posted May 12, 2017 Yeah, Minecraft::getMinecraft() is client-side only. As is Minecraft class in general. Use FMLCommonHandler::getMinecraftServerInstance() to obtain the server in general if you need it, or DimensionManager::getWorld(int id) to get a specific WorldServer. All networking is done on a specific thread so you should not do anything on the networking thread that is world/entities related. Wrap your actions in a Runnable and pass that runnable to a valid IThreadListener using IThreadListener::addScheduledTask(Runnable). A IThreadListener is either a WorldServer instance(server-side) or Minecraft instance(client-side) Quote
BlameTaw Posted May 12, 2017 Author Posted May 12, 2017 Ah okay that makes sense! So do I have to have the world ID passed in with the BlockPos in order to know which world to get? I see that both MinecraftServer and DimensionManager require an ID. Thanks for all the quick responses! Sorry for so many questions, I'm still very new to this API. Quote
V0idWa1k3r Posted May 12, 2017 Posted May 12, 2017 Yes, you do need the world/dimension ID. It can be obtained from the World's provider object(yourWorld.provider.getDimension()). Quote
BlameTaw Posted May 12, 2017 Author Posted May 12, 2017 Fantastic, works like a charm! Thank you for all the help. I feel like I understand a lot more of the flow of this now. Quote
V0idWa1k3r Posted May 13, 2017 Posted May 13, 2017 (edited) Well, to be fair, if you simply want your items to be dropped upon your gui being closed: if you are using a custom Container for your inventory you can simply override container's onContainerClosed method and drop your items there. No packets needed as Container is server-sided. Granted, that will only work if you are using a container in the first place You haven't specified if you are using a container or not in your question, so I haven't thought about it immidiately Edited May 13, 2017 by V0idWa1k3r Quote
BlameTaw Posted May 13, 2017 Author Posted May 13, 2017 (edited) On 5/13/2017 at 12:27 AM, V0idWa1k3r said: Well, to be fair, if you simply want your items to be dropped upon your gui being closed: if you are using a custom Container for your inventory you can simply override container's onContainerClosed method and drop your items there. No packets needed as Container is server-sided. Granted, that will only work if you are using a container in the first place You haven't specified if you are using a container or not in your question, so I haven't thought about it immidiately Expand Oh well maybe I'll try putting it there then... In the mean time, I just found that if you save/quit and reopen the game after modifying the TE's inventory through the GUI, the changes aren't saved so you can duplicate items... Why would changes to the slots not be happening server-side? Isn't that handled by the slot-click method in either Container or GuiContainer? (I forget what it's called and which one) Edit: it DOES make the proper item changes when ejecting items, it's only when manually moving stacks that changes are not made server-side. Edited May 13, 2017 by BlameTaw Quote
V0idWa1k3r Posted May 13, 2017 Posted May 13, 2017 (edited) You are either not changing the inventory of your tileentity in your packet handling (as in you are spawning items in the world but not settings the itemstacks to empty in your tile entity... or setting them to empty but only on client side) or there is an error in saving/loading your Tile's inventory. You can debug those two spots in your code using breakpoints to see where the problem lies Edited May 13, 2017 by V0idWa1k3r Quote
BlameTaw Posted May 13, 2017 Author Posted May 13, 2017 In that case, maybe I have no idea how to ensure my entity's packet handling is correct...It's certainly something I didn't fully understand when researching about it. Here is everything I have for the packet handling in my tile entity class. Is there more that I need than just this? I will fully admit that I copy-pasted this from an example class I found, though I did make an effort to understand what it is doing. I assumed these functions are being automatically called by the Container and GuiContainer manipulations. Am I wrong? // // When the world loads from disk, the server needs to send the TileEntity information to the client // // it uses getUpdatePacket(), getUpdateTag(), onDataPacket(), and handleUpdateTag() to do this @Override @Nullable public SPacketUpdateTileEntity getUpdatePacket(){ NBTTagCompound updateTagDescribingTileEntityState = getUpdateTag(); final int METADATA = 0; return new SPacketUpdateTileEntity(this.pos, METADATA, updateTagDescribingTileEntityState); } @Override public void onDataPacket(NetworkManager net, SPacketUpdateTileEntity pkt) { NBTTagCompound updateTagDescribingTileEntityState = pkt.getNbtCompound(); handleUpdateTag(updateTagDescribingTileEntityState); } /* Creates a tag containing the TileEntity information, used by vanilla to transmit from server to client Warning - although our getUpdatePacket() uses this method, vanilla also calls it directly, so don't remove it. */ @Override public NBTTagCompound getUpdateTag(){ NBTTagCompound nbtTagCompound = new NBTTagCompound(); writeToNBT(nbtTagCompound); return nbtTagCompound; } /* Populates this TileEntity with information from the tag, used by vanilla to transmit from server to client Warning - although our onDataPacket() uses this method, vanilla also calls it directly, so don't remove it. */ @Override public void handleUpdateTag(NBTTagCompound tag){ this.readFromNBT(tag); } Quote
V0idWa1k3r Posted May 13, 2017 Posted May 13, 2017 Oh, when you are manually moving items? Then most likely there is an error in your container implementation. How exactly are you opening you gui + container? You need to do it using your proxy, providing a Container instance for the server and a GuiContainer instance for your client. Quote
BlameTaw Posted May 13, 2017 Author Posted May 13, 2017 I have an IGuiHandler that gets the server/client GUI elements. package com.blametaw.gui; import com.blametaw.itembuffers.blocks.TileEntityBasicBuffer; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.tileentity.TileEntity; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; import net.minecraftforge.fml.common.network.IGuiHandler; public class BasicBufferGuiHandler implements IGuiHandler { private static final int GUIID = 1; public static int getGuiID(){return GUIID;} @Override public Object getServerGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z) { if (ID != getGuiID()) { System.err.println("Invalid ID: expected " + getGuiID() + ", received " + ID); } BlockPos xyz = new BlockPos(x, y, z); TileEntity tileEntity = world.getTileEntity(xyz); if (tileEntity instanceof TileEntityBasicBuffer) { TileEntityBasicBuffer tebb = (TileEntityBasicBuffer) tileEntity; return new ContainerBasicBuffer(player.inventory, tebb); } return null; } @Override public Object getClientGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z) { if (ID != getGuiID()) { System.err.println("Invalid ID: expected " + getGuiID() + ", received " + ID); } BlockPos xyz = new BlockPos(x, y, z); TileEntity tileEntity = world.getTileEntity(xyz); if (tileEntity instanceof TileEntityBasicBuffer) { TileEntityBasicBuffer tebb = (TileEntityBasicBuffer) tileEntity; return new GuiContainerBasicBuffer(new ContainerBasicBuffer(player.inventory, tebb)); } return null; } } And I'm calling it with this function in my Block: @Override public boolean onBlockActivated(World worldIn, BlockPos pos, IBlockState state, EntityPlayer playerIn, EnumHand hand, EnumFacing side, float hitX, float hitY, float hitZ){ if (worldIn.isRemote) return true; playerIn.openGui(ItemBuffers.instance, BasicBufferGuiHandler.getGuiID(), worldIn, pos.getX(), pos.getY(), pos.getZ()); return true; } From what I understand, this should be opening it on both server and client. Also, I'd be surprised if this was the issue because moving things from the TE slots into the player inventory slots DOES work, and those changes are persistent. It's just that quit/reload resets the TE's NBT data. Also there are GUI buttons that update some other values in the NBT data, so I assume that should be done with IMessages as well, right? I didn't think about that before... When you quit/reload those numbers are also reset which implies they're also only happening client side. I've attached a picture of my GUI to give you more of an idea what I'm referring to. That might help a bit. Quote
V0idWa1k3r Posted May 13, 2017 Posted May 13, 2017 Yes, the buttons need an IMessage to sync their presses. I'm not really sure why that issue is happening then. Try just debugging your code in 'problematic' spots that could potentially be causing the issue. See if any TileEntity data is persistent at all upon reloading the world. If it is not then the problem is within saving/loading your tileentity. Also check the log for any error messages - your tile entity will not be saved if it is not registered with GameRegistry::registerTileEntity. Container indeed handles all slot itemstacks changes, and your IGuiHandler seems fine. Quote
BlameTaw Posted May 13, 2017 Author Posted May 13, 2017 (edited) I am registering the TileEntity. No errors in the logs. From some trace messages in the writeToNBT and readFromNBT functions it seems that the client never calls either function, and the server only ever calls the writeToNBT function...That's confusing to me. Okay I feel like I have messed up a lot of things...I'll have to play around with this a bit and get back to you. Whoops. Edited May 13, 2017 by BlameTaw Quote
Recommended Posts
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.