Messages are predefined user actions that can be submitted to Cardinal and handled within a system. A message is the primary way of representing possible user actions in the game. After a message is processed within a tick, it returns a Reply payload that can be read by the game client.

In Cardinal, messages are defined using a pair of Go structs representing a Message and a Reply.


  • An AttackPlayerMsg message may contain the TargetNickname of the player you want to attack.
  • A MoveMsg message may contain the Direction and Distance of the move.

Defining Messages

By convention, messages are defined in the msg directory, with each message definition in its own separate file.

You can easily create a new message and register it to the world by following these steps:


Define the message and reply struct

A message request and and its reply are defined as a Go structs.

package msg

type AttackPlayerMsg struct {
    TargetNickname string

type AttackPlayerMsgReply struct {
    Damage int

Register the message in the world

Messages must be registered in the world before they can be used. This is done by calling the RegisterMessage function.

package main

import (

func main() {
    w, err := cardinal.NewWorld()
    if err != nil {
        log.Fatal().Err(err).Msg("failed to create world")

    // Register messages (user action)
    // NOTE: You must register your message here for it to be executed.
    err := cardinal.RegisterMessage[msg.AttackPlayerMsg, msg.AttackPlayerMsgReply](w, "attack-player")
    if err != nil {
        log.Fatal().Err(err).Msg("failed to register message")

    // ...

Message Options

EVM Support

Messages can be submitted by EVM smart contracts by using the WithMsgEVMSupport option when you register your messages. This will generate the ABI types necessary for interactions with smart contracts.

import (

cardinal.RegisterMessage[msg.AttackPlayerMsg, msg.AttackPlayerMsgReply](w, "attack-player", 
    message.WithMsgEVMSupport[msg.AttackPlayerMsg, msg.AttackPlayerMsgReply]())

Not all Go types are supported for the fields in your message structs when using this option. See EVM+ Message and Query to learn more.

Common Message Patterns

Iterating over messages

package system

// If AttackSystem is registered with the cardinal World, it will be executed each tick.
func AttackSystem(worldCtx cardinal.WorldContext) error {
    // Iterate over the messages that came in during the previous tick
    return cardinal.EachMessage[msg.AttackPlayerMsg, msg.AttackPlayerMsgReply](
        func(attack cardinal.TxData[msg.AttackPlayerMsg]) (msg.AttackPlayerMsgReply, error) {
            // Handle attack logic here...