Sunday, 22 March 2020

Thread safety with network messages [1.14.4+]

Networking is performed in its own thread.  This means that your MessageHandler needs to be very careful not to access and client-side or server-side objects while it is running in its own thread.

For a brief background on multithreaded applications and their perils, see here.  The short summary is-  if your MessageHandler accesses any client-side or server-side objects, for example World or Minecraft, it will introduce a type of bug called a "race condition" which will lead to random crashes and strange, intermittent bugs and weirdness.

The way to overcome this problem is illustrated in the diagram below.  The essential features are:


  1. When your message arrives and your myMessageHandler.onMessageReceived() is called, you should create a lambda function and add it to a queue of tasks.
  2. It will look something like this:
    public static void onMessageReceived(final AirstrikeMessageToServer message, Supplier<NetworkEvent.Context> ctxSupplier) {
      ctx.enqueueWork(() -> processMessage(message, sendingPlayer));
    }
  3. After adding the lambda to the queue, your handler onMessageReceived() should return.
  4. Later on, the MinecraftServer tick will take your lambda function  off the queue and execute it  in the Server thread, which for this example will call myMessageHandler.processMessage(message, sendingPlayer).
  5. Because your message handler is now running in the server thread, it can call any server-side code it wants.

The protocol for messages sent to the client works in the same way.

For a working example of client and server messages, see here (example 60).



Some notes on the vanilla thread-safety code

The vanilla code uses the same basic strategy as outlined above, with a couple of differences

  • After queuing up the Future task, it uses a ThreadQuickExitException to abort further processing in the Netty thread.  Java coding experts generally regard this as a misuse of Exceptions, which are supposed to be for exceptional conditions only, not routine flow.  It was probably implemented this way to save some programming effort which was presumably in short supply at the time.  The proper way is to have one method for queuing the Runnable task, and a second method for processing the message on the client/server thread; the vanilla method uses the same method for both threads.
  • You will also notice the vanilla code uses thread.isOnExecutionThread().  This is only necessary because of the ThreadQuickExitException.  You don't need it, because unlike vanilla your handler.onMessage() will always be running in the Netty thread and your handler.processMessage() will always be running in the client / server thread.

Anyway the short summary is - ignore the vanilla PacketThreadUtil.checkThreadAndEnqueue(), it is not good programming style.


Client<-->Server communication using your own custom messages (SimpleChannel) [1.14.4+]

Although vanilla uses ClientPlayNetHandler and ServerPlayNetHandler for sending packets, you should not use these when modding.  The SimpleChannel is provided for this purpose.

To skip straight to sample code, click here (example 60).

In many cases, you won’t need to create custom packets to keep your client and server code synchronised.  The vanilla code and Forge code do this in a number of ways already:
  • Creation, Removal, Movement, health, actions etc for Entities.  This is accomplished for new Entities by Forge packet EntitySpawnPacket.  You can define a custom handler for this using EntityRegistration.setCustomSpawning().
  • Removal or placement of Blocks
  • Adding DataWatcher variables to your Entity (see here)
  • Containers (Inventory, furnace, etc)
If none of these meet your needs and you need to send custom messages, there are three key components to know about:
  1. Your Message class which represents the message you want to send (for example - a client message to the server might indicate "launch a projectile at location [x,y,z]).  The Message must provides methods to
    * convert the member variables to a serialised format (PacketBuffer) for transmission, and
    * convert the PacketBuffer back to member variables on the receiving side
  2. A MessageHandler class, which is called on the receiving side when your Message arrives for processing
  3. SimpleChannel, which you use to define a channel for your communications as well as to register your Message and MessageHandler.
The Message and MessageHandler are closely coupled.  When registering them with SimpleNetworkWrapper, you need to provide a unique byte ID to identify the message type and its corresponding handler.

The generation, transmission, and receipt of a message (from Client to Server) is illustrated in the diagram below.  Sending a message from Server to Client follows the same pattern.

WARNING! The networking code is multithreaded and your MessageHandler needs to be very careful what it does to avoid crashing the game or introducing subtle, intermittent problems.  See here.



SimpleChannel also provides a number of methods you can use to send messages to the appropriate audience, for example

  • simpleChannel.send(PacketDistributor.PLAYER.with(playerMP), new MyMessage());  // Sending to one player
  • simpleChannel.send(PacketDistributor.TRACKING_CHUNK.with(chunk), new MyMessage()); // Send to all players tracking this chunk
  • simpleChannel.send(PacketDistributor.ALL.noArg(), new MyMessage()); // Sending to all connected players
  • etc

Vanilla Client<-->Server Communication [1.14.4+]

Vanilla communication between the Client and Server sides takes place through Packets, which are sent back and forth using NetHandlerPlayClient and NetHandlerPlayServer.  The basic steps are shown in the diagram below.  There are a large number of different packets used:
* Packets from Client to Server
* Packets from Server to Client
o   Changes to Entities
o   Changes to the World /blocks
o   Managing Graphical User Interface for Containers
o   Miscellaneous


Key points to note

  • Packets sent from Client to Server start with C, for example CPlayerDiggingPacket.
  • Packets sent from Server to Client start with S, for example SUpdateHealthPacket
  • It is possible to create vanilla packets and send them using the NetHandler, however it's almost never necessary.  If you call the correct vanilla methods, they will send the packets for you.  (for example PlayerList.sendMessage() instead of SChatPacket).
  • In order to send your own custom messages between Client and Server, use the SimpleChannel instead of Packets.

Thursday, 12 March 2020

Minecraft Model Basics - Rotation [1.14.4+]


The Renderer for an Entity can use one or more Models, eg head, body, legs.
Each model can be made of multiple boxes (eg the head of a pig has a skull and a snout).
A parent model can also have child models (eg a pig’s head might have ears which can rotate independently of the parent.)






Rotation of the parent model (and children if any) is manipulated using setRotationPoint and rotateAngleXYZ on each model.










Minecraft Model Basics [1.14.4+]


The purpose of this post is to explain the basics of how Minecraft Models (eg PigModel) are constructed .

Models are built up of boxes.  Each box has a size and a texture.

The models used for Entities and some TileEntityRenderers can be a bit confusing, mostly because the model coordinate system is different to the world coordinate system.  The Z axis points from the "head" of the entity back towards its tail, and the X axis points from its right side towards its left side.

The mapping of the box’s texture follows the pattern below.  The Top and Side are all a simple “unwrapping” of the box sides; the Bottom has the same view as the Top, i.e. the pattern on the bottom that you would see if the top were transparent and you were looking down at the “inside” of the bottom.


 When rendering an Entity, Minecraft applies two transformations to convert the model from model coordinate space into world coordinate space:
1.       Flips the x axis and the y axis
2.       Translates the model up (by 24 model coordinates = 1.5 world blocks) to put its feet on the ground.
What does this mean for you?
If you are creating an Entity:
  •        The feet of the model will be at y = 24, not y = 0, unless you adjust it using setRotationPoint().


If you are rendering an Entity model from a TileEntityRenderer:
  •        You need to account for the transformations by performing

matrixStack.scale(-1, -1, 1);
model.preRenderCallback(entityIn, matrixStackIn, partialTicks);  // for LivingRender
matrixStack.translate(0.0, -1.501, 0.0);
before calling the model render.  See LivingRenderer::render
  •        Beware - any subsequent translations will be in model space not world space, i.e. if you translate with matrixStack.translate(0, 1, 0) then the model will be rendered closer to the ground (moved down, not up), because the y axis has been flipped.
A great tool for making minecraft models is BlockBench.  You can do it manually for simple models, but BlockBench makes it much easier and faster to get it right.




Saturday, 15 February 2020

Block Shapes (VoxelShapes) [1.14.4+]



Something that’s important to understand for non-full-cube blocks is the concept of their shape.
The image that you see on the screen (when a block is rendered) is determined by the block model (i.e. the model json file).  But Minecraft also uses a number of other ‘shapes’ to control the interaction of the block with its environment and with the player. Unlike block models, which are defined using a configuration file (json), these other shapes are defined in the code for the Block class itself.
The class which describes a block’s shape is VoxelShape, which you can visualise as a 3-dimensional grid where each grid location (“voxel”) is either full or empty.  Minecraft uses code to build up the VoxelShape from 3D rectangular boxes (cuboids);

VoxelShape being built up from two components using VoxelShapes.or()



 
Creating a VoxelShape with holes using VoxelShapes.combineAndSimplify() with IBooleanFunction.ONLY_FIRST

The coordinate system for the VoxelShape is the same as for models, i.e. a 1x1x1 cubic metre block is 16 x 16 x 16.

VoxelShape coordinate system

Minecraft uses four different types of shape to characterise the block:

BlockState.getShape()

This is the “default” shape of the block.  It is the shape used to draw the outline of the block when your player is directly looking at it.  In most cases, this is the shape used for the other three types of block shapes as well.

Examples of BlockState.getShape()

BlockState.getCollisionShape()


This is the shape used to determine how players or entities “collide” with the block.  For example, the collision shape for a fence or a wall extends far above the rendered model.  Blocks with the doesNotBlockMovement() property have no collisionShape.

The BlockState.getCollisionShape() for fences and walls is higher than the block.  This stops players and entities from jumping over.

BlockState.getRenderShape()

The render shape is used for lighting and shadows purposes and for rendering shortcuts (hiding block faces which the player can’t see).  In most cases this is the same as the default shape, but for a few (Skull, Fence) it is different.  I’m not sure of the reason.


BlockState.getShape() in red, compared with BlockState.getRenderShape() which is empty.

BlockState.getRayTraceShape()

RayTrace is the name of the algorithm that Minecraft uses to determine what object the player is looking at when the player uses an item or tries to mine a block.  The algorithm determines which block the player is affecting, as well as the face of the block (east, north, up, etc) being clicked.  For a few blocks (Cauldron, Composter, Hopper, Scaffold) the RayTraceShape is used to modify the raytrace algorithm to return a different result.  I’m not sure why that’s important.

Composter BlockState.getShape() in red compared with the BlockState.getRayTraceShape() in purple.














Friday, 7 February 2020

Blocks [1.14.4+]


Key concepts relating to blocks:

  • The minecraft world is composed of a three-dimensional grid of blocks.  The world grid is only 256 blocks high but extends indefinitely along the two horizontal axes.
  • The blocks are grouped into 'Chunks', which comprise a cuboid of 16x256x16 blocks.
  • Chunks are then grouped into regions; each region is comprised of 32 x 32 chunks.  Regions are stored on disk, one per file.
  • The point of all this grouping is firstly to reduce storage space (Sections which are completely empty are not saved to disk) and secondly to allow chunks to be created and loaded into memory individually as necessary, without having to store areas where the player hasn't explored.
  • The coordinate system used in minecraft is shown below:


See the Direction enum class for a number of constants and utility functions related to the six directions (eg getOpposite(), rotateX(), getFrontOffsetX(), etc). 


At a minimum, each grid location [x,y,z] contains the following information:
  1. An BlockState, which specifies the type of block at that location (eg a BlockDoor) as well as its current state (eg OPEN or CLOSED).
  2. An integer "SkyLight" value from 0 to 15, which specifies the amount of light this block is receiving from the sky.
  3. An integer "BlockLight" value from 0 to 15, which specifies the amount of light this block is receiving from nearby blocks.
These data are not stored in the Block class.   They are stored in an array inside BlockStateContainer objects, inside Chunk objects.  They are accessed through various World methods such as
BlockState blockAtXYZ = world.getBlockState( {x,y,z});
int lightFromSky = world.getLightFor(SKY, {x,y,z});
int lightFromBlocks = world.getLightFor(BLOCK, {x,y,z});

The most important aspect for the modder to understand is the BlockState.  An BlockState is comprised of two components, the Block and its Properties (its state)
  1. The Block class and derived classes do not store unique information about every location in the world, they only store information related to the Block type.  The Block classes hence consist mostly of methods to determine the behaviour of the block and how it is drawn on the screen. These are generally unique but not always (for example: different types of wood share the same LogBlock).  The vanilla Block instances can be found in the Blocks class; for example Blocks.DIRT, Blocks.SAND, Blocks.WATER.
  2. Each Block may also have one or more Properties which describe the different states that the block can be in. For example- LadderBlock has a property called FACING which has four allowable values (NORTH, SOUTH, EAST, or WEST) depending on which direction it is facing.  Some Blocks have multiple properties, such as BedBlock which has
  •   HORIZONTAL_FACING (in HorizontalBlock), with possible values NORTH, SOUTH, EAST, WEST;
  •   PART, with possible values HEAD or FOOT to define which part of the bed this is;
  •   OCCUPIED, possible values TRUE or FALSE;
The combination of a Block and its current state is represented by a BlockState.  For example, one possible state of BlockBed is
block_bed [facing=east, part=head, occupied=false];
The state can be manipulated using
1.   BlockState.get(property) to get the value of a particular property, eg 
    bedFacingDirection = currentBedState.get(FACING); to return EAST.
2.   BlockState.with(property, new value) to return a new state with one of the properties changed to the new target value, eg 
    BlockState newBedState = oldBedState.with(FACING, NORTH);

If you want to set the state to a particular value, you need to start from the base(default) state, for example
BlockState bedFacingNorth = Blocks.bed.getBaseState().with(FACING, NORTH);
It's a bit verbose but that's how vanilla does it.

A number of other data structures can be used to store more information about a particular grid location:
  • TileEntity, which stores custom information such as the text on signs, the list of items in a chest, etc.  Most grid locations do not have a corresponding TileEntity; each chunk contains a list of TileEntities, with an entry for each TileEntity and its [x,y,z] location within the chunk.  The TileEntity at a particular location is accessed using
    TileEntity tileEntityAtXYZ = world.getTileEntity({x,y,z});
  • Entity, used for some special "blocks" such falling blocks (sand, gravel), paintings, item frames.

Further details

Vanilla use a wide range of BlockStateProperties (see the `BlockStateProperties` class) but the basic types are
  • EnumProperty such as  PISTON_TYPE (normal or sticky) for PistonHeadBlock
  • DirectionProperty such as FACING (N, S, E, W) for DoorBlock
  • IntegerProperty such as AGE (0 – 7) for CropsBlock (eg wheat growth stage)
  • BoolProperty – such as OCCUPIED (true/false) for BedBlock.
The vanilla code contains helpers to create your own properties with custom names and values; see the vanilla blocks mentioned above for inspiration.  

For each given Block, a StateContainer instance is used to define the valid states that the corresponding Block can have.  The StateContainer can be accessed using Block.getStateContainer().  For example, the StateContainer for BedBlock contains three properties
-        FACING, with possible values NORTH, SOUTH, EAST, WEST.
-        PART, with possible values HEAD or FOOT.
-        OCCUPIED, possible values TRUE or FALSE.
The StateContainer for your custom Block is created during Block construction by overriding Block.fillStateContainer().  You need to be careful to set the base (default) state in your block's constructor - see BedBlock for an example.

Unlike previous versions of minecraft, you're not restricted to a certain number of bits for the state information.

During the game initialisation phases, a unique instance is created for each block, see Block.registerBlocks(), and the Blocks class.  This means that you can test for equality using the == operator instead of having to use .equals(), for example
boolean thisBlockIsDirt = (blockAtCursor == Blocks.DIRT);
Likewise, there is a unique instance for each BlockState, allowing for example
boolean stateHasChanged = (newBlockState != currentBlockState);
instead of
boolean stateHasChanged = !newBlockState.equals(currentBlockState);