BattleScriptBattleScriptBattleScriptBattleScript
BrowseUploadDocumentationCompiler / Decompiler

BattleScript

A programming language for creating games and control scripts for the BattleCore laser tag system. Created with love ❤️ by Irrelon Software Limited.

The BattleCore laser tag system is currently in development and you can follow the progress over on our Facebook group.

For business development, media or other queries, please contact us at contact@irrelon.com

Links

  • Browse Scripts
  • Upload Script
  • Documentation
  • Online Compiler

Resources

  • Getting Started
  • API Reference
  • Examples
Terms of ServicePrivacy Policy

© 2025 Irrelon Software Limited. All rights reserved.

Home › Browse › Script Details
← Back to Browse

P2P Protocol

By Rob Evans • 11/6/2025

3

Description

This script defines the ESP-NOW multiplayer protocol used by BattleCore, the system that lets players create and join games, start, pause, end sessions, and broadcast game activity as they are playing.

Tags

protocolp2pesp-nowcomms

Script Content

/*
	This file defines a communications protocol and is written in BattleScript. Protocols
	allow the BattleCore system to conditionally interpret and transcode messages based on
	byte data. Use cases are not limited to over-the-air data such as esp-now and lora,
	but can also define protocols for hard-wired transports such as via UART. This allows
	new hardware to	be connected to the BattleCore motherboard's unused serial connector and
	data to be sent and received with that device even though our BattleCore firmware had
	no prior knowledge of the device or how to communicate with it.

	This allows new, custom peripherals to be plugged into your BattleCore device and
	interacted with as if they were part of the original electronics design!
*/
program {
	type "protocol";
	name "P2P";
	author "Irrelon Software Limited";
	version "1.0.0";
}

// Tell the interpreter that we are defining a new protocol and we are calling it "p2p"
// although the name can be anything we want, "p2p" is fitting as it's our esp-now
// message protocol so we are only handling peer-to-peer messages with it.
// Tell BattleCore that we only want to use this protocol for messages using esp-now.
	// Currently available values for transport are the comms systems built into BattleCore:
	//      esp-now - Peer-to-peer comms over a wifi-like network, without the need for a router
	//      uart - Hard-wired serial connection
	//      wifi - Standard WiFi when connected to an access point / WiFi router
	//      lora - Lo(ng)-Ra(nge) comms, send and receive up to 10 kilometres away!
	//      ble - Bluetooth Low Energy, connect with other BLE devices and swap realtime data
	//      ir - Infrared, the primary system for firing shots at each other in laser tag
protocol "p2p" for "esp-now" {
	// Let's define our protocol's message types. We are going to support commands,
	// requests and responses. These are completely arbitrary and your protocol may not
	// even have them or need them.
	//
	//      COMMAND - One-way message (but can signal they must have an acknowledgement).
	//      REQUEST - Like commands but they expect a response back.
	//      RESPONSE - Messages sent back to answer a request.
	enum MessageType {
		UNKNOWN,
		COMMAND,
		REQUEST,
		RESPONSE
	}

	// Here we define the command types we support in our multiplayer game. The markers next to
	// each command document the type of transmission etc:
	//      (P2B) - Peer to broadcast, this sends to ALL ESP-NOW capable devices in range
	//      (P2P) - Peer to peer, this sends to a specific ESP-NOW device by MAC address
	//      (P2PL) - Peer to peer list, this sends to a list of specific ESP-NOW devices by MAC address (usually the devices we know are part of our game)
	//      (ACK) - A reminder that when receiving this message from the sender, we should send a command of type ACKNOWLEDGEMENT back
	enum CommandType {
		UNKNOWN,

		// GENERAL
		ACKNOWLEDGEMENT, // Sent from a recipient when receiving a command that needs an ACK response

		// LOBBY
		HOST_LOBBY_BEACON, // (P2B) Regular beacon signal advertising lobby
		CLIENT_REQUEST_JOIN_LOBBY, // (P2P) Request for client to join lobby
		HOST_RESPONSE_JOIN_LOBBY, // (P2P) Host response to client request to join lobby
		CLIENT_COMMAND_SIGNAL_READY_STATE, // (P2PL) (ACK) Client requests to set their ready state to 1 or 0
		CLIENT_COMMAND_LEAVE_LOBBY, // (P2P) (ACK) Client tells host they are leaving the lobby
		HOST_COMMAND_LEAVE_LOBBY, // (P2P) Host tells client they have been booted from the lobby
		HOST_COMMAND_GAME_SETTINGS_UPDATE, // (P2PL) Host telling clients about a game settings update
		HOST_COMMAND_GAME_PRE_START, // (P2PL) (ACK) Host telling clients the game start countdown start
		HOST_COMMAND_GAME_PRE_START_CANCEL, // (P2PL) (ACK) Host telling clients the game start countdown has been cancelled

		// GAME ADMIN
		HOST_COMMAND_GAME_PAUSED_STATE, // (P2PL) (ACK) Host telling players the game pause state (1 paused, 0 resume)
		CLIENT_COMMAND_GAME_PAUSE, // (P2P) (ACK) Client requests a game pause
		HOST_COMMAND_GAME_ENDED, // (P2PL) (ACK) Host telling players the game is over

		// GAME DATA
		PLAYER_COMMAND_HIT_CONFIRMED, // (P2P) Player sends to player they were hit by, confirming damage or healing was taken
		PLAYER_COMMAND_DOWN_CONFIRMED, // (P2PL) Player broadcasts the are in a downed state (can be revived for a set time period before completely losing life)
		PLAYER_COMMAND_REVIVE_CONFIRMED, // (P2PL) Player broadcasts they were revived from a downed state
		PLAYER_COMMAND_KILL_CONFIRMED, // (P2PL) (ACK) Player broadcasts they were killed by other player, the player that killed them sends an ACK
		PLAYER_COMMAND_RESPAWNED, // (P2PL) Player broadcasts they just respawned
		PLAYER_COMMAND_PERMA_DEATH, // (P2PL) Player broadcasts they have lost all lives and are out of the game completely
	}

	// Each protocol is made up of commands and steps. A step defines any conditionals that must
	// be true before the step is executed, and a list of commands to execute for the step.
	// A step with no conditions (`require` statements) will always be executed.
	// A step with `require` statements will evaluate each require statement in order. Each
	// `require` statement must evaluate to true before the next is evaluated.
	// Once all `require` statements are evaluated true, a step can define `define` statements
	// that describe the data (byte size) to use in the byte stream.
	// If any `require` statement evaluates to false, the step is skipped and the next step
	// is processed
	//
	// A `define` statement is made up of a data type and the variable name to store it in.
	//      Available types are:
	//          string - An indeterminate number of bytes followed by a string terminator byte
	//
	//          Unsigned (negative numbers not allowed):
	//              uint8 - An unsigned integer value between 0 and 255
	//              uint16 - An unsigned integer value between 0 and 65,535
	//              uint32 - An unsigned integer value between 0 and 4,294,967,295
	//
	//          Signed (negative and positive numbers allowed):
	//              int8 - A signed integer value between -128 and 127
	//              int16 - A signed integer value between −32,768 and 32,767
	//              int32 - A signed integer value between −2,147,483,648 and 2,147,483,647

	// Before anything else, we read the first byte of data from the incoming byte stream
	// into the variable "startByte" which we will examine in the next step.
	define uint8 startByte;

	// A require statement will check its condition and if it is false, the block will be
	// exited. If the block is the protocol itself, the whole protocol will be skipped.
	// This allows you to exit processing further protocol commands and steps if a condition
	// is not met.
	require startByte == 23;
	define uint8 messageType;

	// Custom functions must start with $ e.g. $like or $partial
	// or you can use == and != as usual
	step {
		// A require statement inside a step block will only exit the current step if
		// the condition is unmet.
		require messageType == MessageType.COMMAND;
		define uint8 shouldAcknowledge;
		define uint8 commandId;
	}

	// Define data for all request-type messages
	step {
        // When the messageType is a REQUEST
        require messageType == MessageType.REQUEST;
        // Read a byte and store in requestId
        define uint8 requestId;
        // Read a byte and store in commandId
        define uint8 commandId;
    }

    // Define data for all response-type messages
    step {
        // When the messageType is a RESPONSE
        require messageType == MessageType.RESPONSE;
        // Read a byte and store in requestId
        define uint8 requestId; // This is the original request's requestId allowing the receiver (the original request sender) to know what request was responded to
        // Read a byte and store in commandId
        define uint8 commandId;
    }

	// Define data for specific command messages
    step {
        require commandId == CommandType.HOST_LOBBY_BEACON;
        define string name; // The lobby name to show in the UI e.g. "Jim's Game"
        define uint32 time; // The current host timestamp, used for clock synchronisation
    }

    step {
        require commandId == CommandType.CLIENT_REQUEST_JOIN_LOBBY;
        define string name; // The name of the player joining the lobby, shown to all players e.g. "Amelia Earhart"
    }

    step {
        require commandId == CommandType.HOST_RESPONSE_JOIN_LOBBY;
        define uint8 result; // Either 1 for successfully joined or 0 for failed to join
    }

    step {
        require commandId == CommandType.CLIENT_COMMAND_SIGNAL_READY_STATE;
        define uint8 state; // Either 1 for ready or 0 for not ready
    }

    step {
        require commandId == CommandType.CLIENT_COMMAND_LEAVE_LOBBY;
        // No data, this is a message with no payload
    }

    step {
        require commandId == CommandType.HOST_COMMAND_LEAVE_LOBBY;
        define uint8 reason; // An enum integer indicating one of the set reasons for booting the player
    }

    step {
        require commandId == CommandType.HOST_COMMAND_PLAYER_JOINED_LOBBY;
        define uint8 id; // The host assigned player id number from 0 to 255
        define uint8 mac1; // Player mac address byte 1
        define uint8 mac2; // Player mac address byte 2
        define uint8 mac3; // Player mac address byte 3
        define uint8 mac4; // Player mac address byte 4
        define uint8 mac5; // Player mac address byte 5
        define uint8 mac6; // Player mac address byte 6
        define string name; // Player's name
    }

    step {
        require commandId == CommandType.HOST_COMMAND_PLAYER_LEFT_LOBBY;
        define uint8 id; // The host assigned player id number from 0 to 255
    }

    step {
        require commandId == CommandType.HOST_COMMAND_GAME_SETTINGS_UPDATE;
        define string gameId; // ID of the game battlescript file that defines the game code
        define string gameName; // Name of the game type e.g. "Slayer Classic"
        define uint8 isTeamBased; // 0 = all against all, 1 = team based - some games will lock this if their battlescript code doesn't allow one type or another
        define uint32 gameDurationSecs; // How long the game will last in seconds
        define uint8 radarType; // 0 = off, 1 = movement-based, 2 = permanent
        define uint8 mapType; // 0 = unlimited, 1 = limited - if limited, a defined polygon play-area will need to be setup by sending all players out to the edges of the play area
        define uint8 playerLives; // Number of lives players start with
    }

    step {
        require commandId == CommandType.HOST_COMMAND_GAME_PRE_START;
        define uint8 eventTime; // The timestamp of when the countdown started from for timing synchronisation,
        define uint8 countdownSecs; // The number of seconds before the game will auto-start
    }

    step {
        require commandId == CommandType.HOST_COMMAND_GAME_PRE_START_CANCEL;
        // No data, this is a message with no payload
    }

    step {
        require commandId == CommandType.HOST_COMMAND_GAME_PAUSED_STATE;
        define uint8 state; // 1 = paused, 0 = resumed
    }

    step {
        require commandId == CommandType.CLIENT_COMMAND_GAME_PAUSE;
        // No data, this is a message with no payload
    }

    step {
        require commandId == CommandType.HOST_COMMAND_GAME_ENDED;
        define uint8 endType; // 0 = score (player or team reached winning score), 1 = time (time ran out), 2 = condition (everyone died etc), 3 = forced (host forced game to end)
    }

    step {
        require commandId == CommandType.PLAYER_COMMAND_HIT_CONFIRMED;
        define uint8 type; // 0 = damage, 1 = healing
        define uint8 amount; // The amount of damage or healing taken
        define uint8 byId; // The id of the player that caused the event
    }

    step {
        require commandId == CommandType.PLAYER_COMMAND_DOWN_CONFIRMED;
        define uint8 byId; // The id of the player that caused the event
    }

    step {
        require commandId == CommandType.PLAYER_COMMAND_REVIVE_CONFIRMED;
        define uint8 byId; // The id of the player that caused the event
    }

    step {
        require commandId == CommandType.PLAYER_COMMAND_KILL_CONFIRMED;
        define uint8 byId; // The id of the player that caused the event
    }

    step {
        require commandId == CommandType.PLAYER_COMMAND_RESPAWNED;
        // No data, this is a message with no payload
    }

    step {
        require commandId == CommandType.PLAYER_COMMAND_PERMA_DEATH;
        // No data, this is a message with no payload
    }
}