Skip to content
WeAthFoLD edited this page Jan 27, 2016 · 5 revisions

Every modder must be, at some point, VERY annoyed by how much things we have to do to send something across network in vanilla forge. For example if we want to add a value query in client when we open GUI:

  • Create a new Message class
  • Specify how that class reads or writes data
  • Create a corresponding IMessageHandler
  • Create a new discriminator for the channel
  • Register the message and message handler

But all we want to do is just simple information exchange up to one or two values! This is just TOO stupidly redundant. The Message approach is too low-level and inefficient (productivity-wise).

With the help of core S11n API, the Network S11n API provides a way to write or read via netty's ByteBuf seamlessly. It is highly optimized to match the data compactness of hand-writing code, and is extremely simple. Also the API provides a series of ways to conveniently transform information across network, built on top of ByteBuf s11n.

The ByteBuf s11n

The functionality is provided in cn.lambdalib.s11n.network.NetworkS11n singleton class. A quick glance on how it does things:

// Unhintted
public static void serialize(ByteBuf buf, Object obj, boolean nullable);
public static <T> T deserialize(ByteBuf buf);

// Hintted
public static <T> void serializeWithHint(ByteBuf buf, T obj, Class<? super T> type);
public static <T, U extends T> T deserializeWithHint(ByteBuf buf, Class<U> type);

// Behaviour alternation
public static <T> void addDirect(Class<T> type, NetS11nAdaptor<? super T> adaptor);
public static void register(Class<?> type);

The usage is really simple, but to achieve data compactness some small tricky points is worth mentioning.

Hinted and Unhintted

You might have noticed that there are two pairs of serialization routines.

They differ in that unhintted routine (serialize and deserialize) will write the type index into the buffer and retrieve the type in other side by this type index. This shall be used when the object's type is dynamic. hintted routine will emit the type index and directly use the class passed in as the base type. You shall notice that by only by using unhintted s11n can the null be transfered. The unhintted serialization's return value is always not null.

You must use register method on a type to indicate that it will be used in unhintted s11n. Otherwise, trying to serialize that type yields a runtime error.

On recursive serialization

If you are wondering what recursive serialization is, check out S11n core API.

With the statement above, we know that hintted serialization is more data-compact. In recursive serialization, we are trying to use this approach as much as possible, as the type of the field is known. However this will cause the polymorphic behaviour to be lost. For example:

public class Base {}
public class Derived extends Base {}
public class DynamicMsg {
   Base content;
}

if we use serialize on a DynamicMsg, and its "content" field's value is of type Derived, after deserialization we will still get a Base instance.

If you want a field to serialize to it's actual runtime type, mark @SerializeDynamic on the field.

Also, it's strongly disencouraged to serialize null across network. If you intend to do it, mark the field with @SerializeNullable.

Serialization interruption

For simple value classes like Vec3 and Color, we would gladly expect that objects serialized behave as exactly the same as the original one. However, there exists a situation where an object can't be recovered - for example, when we are trying to serialize an entity to client. That client might not have that entity alive, or even in another dimension. In that case, an ContextException is thrown, indicating that we can't find the object at the given side.

Direct types

Directs types are defined by adding using addDirect method, where you supply an object defining how to read or write via ByteBuf.

Note that when reading from the buf, you never return null. If you can't recover the object on other side, you should throw an ContextException.

The helper APIs

The ByteBuf s11n is really helpful - we never have to write anything manually to ByteBuf. But that doesn't solve all the problem because we still have to create IMessage and IMessageHandler, which is verbose. To solve this problem, a series of helper utils are created to ease network messaging, all built on top of ByteBuf s11n.

Network Event Bus

class: cn.lambdalib.s11n.network.NetworkEvent

Implements an simple event bus across network. When an event is sent to any side, the corresponding handlers (classified by object class) are invoked. This is simply a much improved version of the relationship between IMessage and IMessageHandler.

Network Message

class: cn.lambdalib.s11n.network.NetworkMessage

Allows you to channel "message" across network. A message is discriminated by a channel (String), containing a list of parameters. All the methods in the object with @Listener annotation with corresponding side and channel will be invoked with the parameter list. You must, at send time, specify the instance to send the message to.

Note that the instance and all the parameters must be registered in ByteBuf s11n. Otherwise it yields an runtime error.

Usage example:

public class EpicTileEntity extends TileEntity {
    public int fatness;
    
    public void updateEntity() {
        if (!worldObj.isRemote) {
            NetworkMessage.sendToAllAround(TargetPointer.convert(this, 10), this, "sync_fatness", fatness);
        }
    }
    
    @Listener(channel="sync_fatness", side=Side.CLIENT)
    private void syncFatness(int fatness) {
        this.fatness = fatness;
    }
    
    // Note that this works too because parameters will automatically be trimmed.
    @Listener(channel="sync_fatness", side=Side.CLIENT)
    private void syncIndicator() {
        System.out.println("synced");
    }
}

Misc

When using the above API you always have to specify the sync target. When you are trying to send to client the TargetPointHelper util might be very helpful.

Clone this wiki locally