Networking
Preface
The article tries to document the network communication related aspects of M.A.X. that could either
be interesting for an enthusiast or could be relevant for an engineer.
The information found herein is not guaranteed to be complete or technically accurate.
Overview
M.A.X. supports three networking options.
- IPX protocol based local area network play for maximum four human players.
- Dial-up Modem play for two human players.
- Null Modem play for two human players.
The implementations rely on MS-DOS DPMI services and direct I/O port manipulation. None of these are available in modern OS environments anymore.
DOSBox emulates the IPX protocol and related DPMI services and tunnels IPX packets over UDP/IP protocol using a client-server architecture [1]. Dial-up modem emulation supports making and answering calls and routes transmission data over TCP/IP protocol [1].
M.A.X. implements an input-synchronous, lockstep peer-to-peer networking model. For the used terminology see [2]. Turn-based and simultaneous game modes behave differently. Matchmaking uses its own netcode.
There are several design alternatives for M.A.X. Port to consider for the reimplementation:
DOSBox Compatible Model
Keep M.A.X. Port compatible with DOSBox and original M.A.X. v1.04. This would allow easier testing, incremental reimplementation of networking related aspects of the game and cross-play.
With this approach the IPX driver remains the original except for DPMI and direct I/O manipulation interfaces. The game would realize the DOSBox IPX client first that would connect to a DOSBox IPX server configuration. The original M.A.X. v1.04 under DOSBox would connect to M.A.X. Port. Starting from such a setup incremental reimplementation of higher networking layer functionalities would theoretically be easier.
The problem is that this cross-compatibility could become a negative trait pretty fast:
-
First of all fixing defects is not possible as long as the original game remains in the communication loop.
-
DOSBox implements a client-server architecture to emulate IPX networks while M.A.X. uses a peer-to-peer networking model. This means that every message is first sent to the DOSBox IPX server and that distributes messages to their true destinations. The left side depicts message transfers under DOSBox. The right side shows how MAX communicated over IPX LAN originally.
I experienced a lot of desyncs with this setup even on localhost, although maybe they had to do something with my decade old PC, Wireshark sniffing all traffic on its loopback adapter, and four DOSBox instances plus several other memory hungry applications running in parallel. -
The protocol overhead is considerable. The game produces IPX packets. DOSBox wraps IPX packets into UDP/IP packets. The IPX header is 30 bytes long. The game specific IPX Protocol Header adds 8 bytes on top of that. The UDP and IP headers add 20 + 8 bytes overhead or so. The application layer protocol does not stream data. One application layer packet is basically wrapped into one UDP packet at the end. The smallest payload size of an application layer packet is 0 byte while the biggest one is 316 bytes. Based on a network trace of 220k+ packets, 97.74% of those packets were 62 to 79 bytes long. Thats quite a lot of wasted bandwidth due to protocol overhead.
-
There is risk of data corruption. The application layer protocol implements a CRC16 checksum using the CRC16-CCITT polynomial. With a worst case payload size of 316 bytes 4 bit errors might be detected, but not much more. I do not think that this is sufficient for Internet play use case when the networking model depends on 100% deterministic lockstep. This is a potential root case for desyncs.
-
The IPX driver does not implement much error recovery functionality… in most cases the game just exits on errors without any recovery attempt. In case of a packet loss the game makes one attempt to get the missing packet via retransmission, but if that retransmit request is also lost than it is guaranteed game over.
Peer-to-Peer Model
Ditch everything below the Transport layer together with Null Modem, Dial-Up Modem and IPX LAN play and replace all of these with a single UDP/IP based alternative design enabling Internet play.
This approach keeps the original input-synchronous, lockstep peer-to-peer networking model, but removes the overhead of IPX protocol emulation and could switch to a streaming buffer or could use redundant transfers to balance bandwidth and packet retransmission round trip times or could introduce a CRC32 checksum for added plausibility or whatever that helps to gain stability and robustness against network errors.
Potential problem sources:
-
Broadcast messaging is currently a requirement for the application layer. If Internet play is targeted this must be worked around.
-
Multicast messaging would be highly beneficial to use due to the peer-to-peer model of the game, but it is not viable for Internet play. It makes no sense to implement different protocols for LAN and Internet play.
-
With a peer-to-peer setup matchmaking will be interesting over the Internet.
Client-Server Model
This is not viable or meaningful as long as the game architecture is not adapted to such an architecture. Security is a huge concern for such a setup. Server hosting and maintenance costs need to be considered.
Documentation
The following chapters only document the IPX protocol based networking option. Dial-up and Null Modem specific features and protocols are not in scope.
Internet Layer Protocols
IPX Protocol
IPX is a connectionless datagram protocol. Datagram means that each packet is treated as an individual entity, having no logical or sequential relation to any other packet [3].
This section describes the game specific protocol that is built above Novell’s IPX protocol. From now on the IPX protocol term will refer to the game specific feature set that incorporates the IPX protocol itself.
In the case of IPX protocol the game distinguishes 3 types of message transmission mode:
- Unicast: The message is sent to a single destination.
- Multicast: The message is sent to all known destinations one by one as IPX protocol does not support real multicast messaging.
- Broadcast: The message is sent to all nodes on the local network using the IPX broadcast address.
The Internet or Network layer protocol of M.A.X. built above Novell’s IPX protocol implements:
- organization of nodes into virtual communication channels
- packet verification
- packet filtering
- packet loss detection
- packet retransmission
- message (re)ordering
- message buffering
- basic transmit rate limitation
The IPX protocol layer is not responsible for:
- node discovery
- node address allocation or (re)assignments
- round trip time measurements
- timeout monitoring
DPMI services that interface with the system’s IPX driver use so called Event Control Blocks (ECBs) to send and receive packets. The game registers 4 transmit ECBs and 28 receive ECBs.
To transmit a packet the game uses DPMI service INT 7A, function 3 (IPX Driver, SEND PACKET). The service is asynchronous, non blocking. Transmit requests are rate limited to 1 millisecond per packet. The transmit ECB slots are operated like a ring buffer. If the current ECB slot is still busy the game enters a busy wait loop without looking for another slot that might already be free. If the completion code of the ECB that was in the given ECB slot previously indicates any kind of protocol error the game terminates itself immediately.
To receive a packet DPMI service INT 7A, function 4 (IPX Driver, LISTEN FOR PACKET) is used. The function is non blocking, it just registers the ECB slot to be used for reception. Reception is polling based. The game checks in a loop whether any of the Rx ECBs are in ready state.
Application Layer Protocols
The application layer protocols act in a connectionless peer-to-peer network topology. There are two types of actors, a host and its clients.
-
Matchmaking This protocol is used to set up multiplayer games. It has its own set of packets and most transmissions are broadcast type as the IPX local node addresses of actors are not yet known. It is capable of distinguishing multiple hosts and keeping track which clients belong to which lobby.
-
Turn Based Play This protocol implements an input-synchronous lockstep architecture.
-
Simultaneous Moves Play This is very similar to the turn based play protocol, but its lockstep implementation differs.
Matchmaking
Typical message sequence when a Client joins a Host during game setup:
Typical message sequence when the Host starts a configured game with two Clients:
Turn Based Play
Typical message sequence when players make their initial mission supplies configuration and select a landing zone:
Simultaneous Moves Play
Packet Structures
The transport protocol layer wraps M.A.X. packets with IPX or Modem / Serial protocol specific meta data.
All M.A.X. specific formats use little endian byte order.
IPX Packet - IPX Protocol Header
- Packet Number (4 bytes): Only 6 LSBs are used in the value range 0 - 63. Default value is 0. The field is only incremented after an actual game is started by the Host. Each player’s counter is managed individually.
- Session ID (2 bytes): The default value is 0x913F. The value is set to a random number when the Host starts a game. See Packet 32.
- CRC16 (2 bytes): The checksum is calculated over the M.A.X. Packet structure and the first byte of the packet number field.
Modem Packet
- Packet Number (1 byte): Only 6 LSBs are used in the value range 0-63. Default value is 0. The field is only incremented after an actual game is started by the Host. Each player’s counter is managed individually.
- Packet Length (2 bytes): Size of the M.A.X. Packet in bytes.
- CRC16 (2 bytes): The checksum is calculated over the M.A.X. Packet structure and finally two additional bytes are fed to the CRC16 algorithm, 0x00 and 0xFF.
M.A.X. Packet
- Packet Type (1 byte): Type of data structure found in the Packet Data field. See M.A.X. Packet Types section.
- Entity ID (2 bytes): Packet Type specific meaning. E.g. Unit hash key or Node address of game client.
- Packet Length (2 bytes): Size of Packet Data field in bytes.
- Packet Data (0 - 550 bytes): Packet Type specific content.
The node address is an application layer address of the game client.
Node addresses 0 - 9 have special meaning.
Node address 0: Red Team (Player 0).
Node address 1: Green Team (Player 1).
Node address 2: Blue Team (Player 2).
Node address 3: Gray Team (Player 3).
Node address 4: Alien Derelicts (Player 4).
Normal addresses are calculated as rand(0 to 32767) * 31991 >> 15 + 10
. The game implements its own pseudo random number generator. The host allocates all node addresses. The previous formula is rerolled as long as the resulting node address is not yet defined for any registered players.
CRC16 Algorithm
Initial CRC value is 0x0000.
void crc16(unsigned short *crc, unsigned char c) {
int i;
int carry;
for (i = 0; i < 8; ++i) {
carry = *crc & 0x8000;
*crc <<= 1;
if (c & 0x80) {
(*crc)++;
}
c <<= 1;
if (carry) {
*crc ^= 0x1021;
}
}
}
M.A.X. Packet Types
M.A.X. v1.04 defines 54 packet types, although some of them are not implemented.
Wireshark Generic Dissector (wsgd) profile for DOSBox M.A.X. v1.04 IPX LAN network play.
Packet Header
All fields are encoded in little endian byte order.
struct MAX_PacketHeader
{
byte_order little_endian;
MAX_PacketTypeId packet_type;
uint16 entity_id;
uint16 data_size;
}
packet_type Packet type ID. See below list of packets.
entity_id Packet Type specific meaning. E.g. Unit hash key or Node address of game client.
data_size Size of the packet type specific data that follows the packet header.
Packet 00
struct MAX_Packet_00
{
MAX_PacketHeader Header;
uint8 field_0;
}
Packet 01
struct MAX_Packet_01
{
MAX_PacketHeader Header;
uint8 frame_id;
}
Packet 02
Only implemented in early game versions.
Packet 03
Only implemented in early game versions.
Packet 04
Only implemented in early game versions.
Packet 05 - Announce IPX Local Address
IPX unicast message sent by the client to the host to acknowledge Packet 32 and to announce the client’s IPX local address to the host. When the host received all client’s IPX local address via this message it responds with Packet 34 to each client.
struct MAX_Packet_05
{
MAX_PacketHeader Header;
MAX_TeamType team;
uint16 field_2;
MAX_IpxAddress address;
}
entity_id Sender’s team slot (node address 0 - 3).
team Team slot of the sender (client).
field_2 Always set to 0x0000. Note that the packet format of Packet 32 and Packet 05 are the same so the field is basically reserved.
address Local IPX address of the client. See Packet 29.
Packet 06 - End Turn
IPX multicast message sent by the player that ended their turn to all other players as an event notification.
struct MAX_Packet_06
{
MAX_PacketHeader Header;
uint8 field_0;
}
entity_id Sender’s team slot (node address 0 - 3).
field_0 Always set to 0x0000. Note that the field is not used for anything, it is basically reserved.
Packet 07 - Exit Game
IPX multicast message sent by the player that leaves the game to all other players. The player that sends the request to exit the game first increments their own request_uid counter. Other players respond with Packet 48.
struct MAX_Packet_07
{
MAX_PacketHeader Header;
uint8 request_mode;
uint8 request_uid;
}
entity_id Sender’s team slot (node address 0 - 3).
request_mode Either 0 or 1. TODO: figure out meaning.
request_uid Unique request identifier. Initial value is 0x00.
Packet 08 - Unit Order
IPX multicast message sent by player that issued an order to all other players.
There are 32 different orders in M.A.X. v1.04.
struct MAX_OrderCategory_01
{
uint16 parent_unit_id;
uint16 target_grid_x;
uint16 target_grid_y;
uint16 enemy_unit_id;
uint8 repeat_build;
uint8 build_time;
uint8 build_rate;
uint16 list_size;
MAX_UnitType[list_size] build_orders;
}
struct MAX_OrderCategory_02
{
uint16 parent_unit_id;
uint16 target_grid_x;
uint16 target_grid_y;
uint16 enemy_unit_id;
}
struct MAX_OrderCategory_03
{
uint16 parent_unit_id;
uint16 target_grid_x;
uint16 target_grid_y;
uint16 enemy_unit_id;
uint8 total_mining;
uint8 raw_mining;
uint8 fuel_mining;
uint8 gold_mining;
}
struct MAX_OrderCategory_04
{
uint16 parent_unit_id;
}
struct MAX_Packet_08
{
MAX_PacketHeader Header;
MAX_Orders order;
uint8 state;
MAX_Orders prior_order;
uint8 prior_state;
uint8 disabled_reaction_fire;
switch(order)
{
case MAX_Orders::AWAITING :
case MAX_Orders::TRANSFORMING : MAX_OrderCategory_01 order_data;
case MAX_Orders::MOVING : MAX_OrderCategory_02 order_data;
case MAX_Orders::FIRING : MAX_OrderCategory_02 order_data;
case MAX_Orders::ORDER_BUILDING : MAX_OrderCategory_01 order_data;
case MAX_Orders::ACTIVATE_ORDER : MAX_OrderCategory_02 order_data;
case MAX_Orders::NEW_ALLOCATE_ORDER : MAX_OrderCategory_03 order_data;
case MAX_Orders::POWER_ON :
case MAX_Orders::POWER_OFF :
case MAX_Orders::EXPLODING :
case MAX_Orders::UNLOADING : MAX_OrderCategory_04 order_data;
case MAX_Orders::CLEARING : MAX_OrderCategory_01 order_data;
case MAX_Orders::SENTRY :
case MAX_Orders::LANDING :
case MAX_Orders::TAKING_OFF :
case MAX_Orders::LOADING :
case MAX_Orders::IDLE :
case MAX_Orders::REPAIRING : MAX_OrderCategory_04 order_data;
case MAX_Orders::REFUELING : MAX_OrderCategory_04 order_data;
case MAX_Orders::RELOADING : MAX_OrderCategory_04 order_data;
case MAX_Orders::TRANSFERRING : MAX_OrderCategory_02 order_data;
case MAX_Orders::AWAITING_21 :
case MAX_Orders::AWAITING_22 :
case MAX_Orders::AWAITING_23 :
case MAX_Orders::AWAITING_24 : MAX_OrderCategory_02 order_data;
case MAX_Orders::AWAITING_25 : MAX_OrderCategory_02 order_data;
case MAX_Orders::DISABLED : MAX_OrderCategory_01 order_data;
case MAX_Orders::MOVING_27 : MAX_OrderCategory_02 order_data;
case MAX_Orders::REPAIRING_28 : MAX_OrderCategory_04 order_data;
case MAX_Orders::TRANSFERRING_29 :
case MAX_Orders::ATTACKING : MAX_OrderCategory_02 order_data;
case MAX_Orders::BUILDING_HALTED :
}
}
entity_id The unit’s UnitHash type hash key for which the order applies to.
TODO: Describe all fields.
Packet 09 - TODO
IPX multicast message.
struct MAX_ResearchTopic
{
uint32 research_level;
uint32 turns_to_complete;
uint32 allocation;
}
struct MAX_Packet_09
{
MAX_PacketHeader Header;
uint16 team_credits;
uint16 credits_spent;
uint8[40] field_4;
string(30) team_name;
MAX_ResearchTopic[8] research_topics;
}
entity_id Sender’s team slot (node address 0 - 3).
TODO: Describe all fields.
Packet 10 - Unit Type Upgrade
IPX multicast message sent by a player to all other players to inform them about a unit type specific upgrade. The UnitValues class members are sent to the other players. This packet is sent also when an upgrade is made within the Purchase Menu before starting a game.
If credits are spent to make an upgrade, then Packet 09 is also sent to update the gold_spent metric.
enum16 MAX_UnitType
{
COMMTWR 0x0
POWERSTN 0x1
POWGEN 0x2
BARRACKS 0x3
SHIELDGN 0x4
RADAR 0x5
ADUMP 0x6
FDUMP 0x7
GOLDSM 0x8
DEPOT 0x9
HANGAR 0xA
DOCK 0xB
CNCT_4W 0xC
LRGRUBLE 0xD
SMLRUBLE 0xE
LRGTAPE 0xF
SMLTAPE 0x10
LRGSLAB 0x11
SMLSLAB 0x12
LRGCONES 0x13
SMLCONES 0x14
ROAD 0x15
LANDPAD 0x16
SHIPYARD 0x17
LIGHTPLT 0x18
LANDPLT 0x19
SUPRTPLT 0x1A
AIRPLT 0x1B
HABITAT 0x1C
RESEARCH 0x1D
GREENHSE 0x1E
RECCENTR 0x1F
TRAINHAL 0x20
WTRPLTFM 0x21
GUNTURRT 0x22
ANTIAIR 0x23
ARTYTRRT 0x24
ANTIMSSL 0x25
BLOCK 0x26
BRIDGE 0x27
MININGST 0x28
LANDMINE 0x29
SEAMINE 0x2A
LNDEXPLD 0x2B
AIREXPLD 0x2C
SEAEXPLD 0x2D
BLDEXPLD 0x2E
HITEXPLD 0x2F
CONSTRCT 0x31
SCOUT 0x32
TANK 0x33
ARTILLRY 0x34
ROCKTLCH 0x35
MISSLLCH 0x36
SP_FLAK 0x37
MINELAYR 0x38
SURVEYOR 0x39
SCANNER 0x3A
SPLYTRCK 0x3B
GOLDTRCK 0x3C
ENGINEER 0x3D
BULLDOZR 0x3E
REPAIR 0x3F
FUELTRCK 0x40
CLNTRANS 0x41
COMMANDO 0x42
INFANTRY 0x43
FASTBOAT 0x44
CORVETTE 0x45
BATTLSHP 0x46
SUBMARNE 0x47
SEATRANS 0x48
MSSLBOAT 0x49
SEAMNLYR 0x4A
CARGOSHP 0x4B
FIGHTER 0x4C
BOMBER 0x4D
AIRTRANS 0x4E
AWAC 0x4F
JUGGRNT 0x50
ALNTANK 0x51
ALNASGUN 0x52
ALNPLANE 0x53
ROCKET 0x54
TORPEDO 0x55
ALNMISSL 0x56
ALNTBALL 0x57
ALNABALL 0x58
RKTSMOKE 0x59
TRPBUBLE 0x5A
HARVSTER 0x5B
WALDO 0x5C
}
struct MAX_Packet_10
{
MAX_PacketHeader Header;
MAX_UnitType unit_type;
uint16 turns;
uint16 hits;
uint16 armor;
uint16 attack;
uint16 speed;
uint16 range;
uint16 rounds;
uint16 move_and_fire;
uint16 scan;
uint16 storage;
uint16 ammo;
uint16 attack_radius;
uint16 agent_adjust;
}
entity_id Sender’s team slot (node address 0 - 3).
unit_type See MAX_UnitType.
turns Turns required to build the unit type.
hits Base health points of the unit type.
armor Armour rating of the unit type.
attack Attack rating of the unit type.
speed Base movement points of the unit type.
range Attack range of the unit type. Only meaningful if the unit has rounds > 0.
rounds Shots available for the unit type per game turn.
move_and_fire True or False. Available shots are not decreased if movement points are spent.
scan Scan range of the unit type.
storage Base storage capacity of the unit type. TODO: Document infiltrator use case.
ammo Base ammunition of the unit type.
attack_radius Area attack radius. 0 means a single square at the attack cursor. 1 means 9 squares in a rectange. 2 means 25 squares in a rectange. The rocket launcher is the only unit where this is non zero.
agent_adjust TODO: Document infiltrator use case.
Packet 11 - Update Complex
IPX multicast message sent by the player to every other player.
For each complex of the player a dedicated message is sent. At the beginning of each turn resource consumption optimizations are made which could lead to change of resources available in a complex.
struct MAX_Packet_11
{
MAX_PacketHeader Header;
uint16 id;
int16 material;
int16 fuel;
int16 gold;
int16 power;
int16 workers;
int16 buildings;
}
entity_id Sender’s team slot (node address 0 - 3).
id Complex identifier starting with index 1.
material Raw materials resource available in the complex.
fuel Fuel resource available in the complex.
gold Gold resource available in the complex.
power Power resource available in the complex. This is the available surplus only.
workers Workers resource available in the complex. This is the available surplus only.
buildings Number of buildings in the complex.
Packet 12 - Select Landing Zone
IPX multicast message sent by player to every other player during landing zone selection at the beginning of a game.
Overlapping landing zones are evaluated by each player by an algorithm that places supplied and purchased units around the selected starting location.
struct MAX_Point
{
byte_order little_endian;
uint16 x;
uint16 y;
}
struct MAX_MissionSupply
{
byte_order little_endian;
MAX_UnitType unit_type;
uint16 unit_storage;
}
struct MAX_Packet_12
{
MAX_PacketHeader Header;
uint16 field_0;
uint16 credits;
uint16 unit_count;
uint16 credits_spent;
MAX_Point start_position;
uint8 proximity_state;
MAX_MissionSupply[unit_count] units;
}
entity_id Sender’s team slot (node address 0 - 3).
field_0 TODO: Unknown field.
credits The sum of the starting credits and the clan credits. Note that none of the clans have credits in M.A.X. v1.04.
unit_count Number of units supplied and purchased for the mission.
credits_spent Credits spent in the purchase menu. The value is tracked by the game for mission statistics that are shown at the end of the game.
start_position Grid coordinates selected as landing zone for the team. The given position is the top left point of a 3 by 3 rectange. The initial mining station and its power generator are not found in the units array. Units in the array are placed from top left corner in a clock wise order in the 3 by 3 rectangle. If there are more than four units a 5 by 5 rectangle is filled up again starting from top left corner and so on to 7 by 7 rectangle. The maximum data_size is 550 bytes so in theory 134 units can be allocated to a team at game startup. Of course M.A.X. v1.04 does not provide enough starting credits to do so unless someone deliberately uses one of the game defects as an exploit.
proximity_state Initially set to 1. If proximity zones overlap an additional confirmation is required from the affected players to stay. If a player remains within the original landing zone the value is set to 4, otherwise a new landing zone is selected with value 1. The player that made a valid selection locks its position with a value of 5 in a new message.
units Array of units and their stored materials corresponding to the supplied and purchased units list.
Packet 13 - Update RNG Seed
IPX multicast message sent by the host to each client right after Packet 34.
struct MAX_Packet_13
{
MAX_PacketHeader Header;
uint32 rng_seed;
}
entity_id Sender’s team slot (node address 0 - 3).
rng_seed The field value is used by game clients to set the RNG seed value via srand(). The value is derived from the time() C-API function from <time.h>. The time function determines the current calendar time which represents the time since January 1, 1970 (UTC) also known as Unix epoch time.
Packet 14 - TODO
IPX multicast message. TODO
struct MAX_Packet_14
{
MAX_PacketHeader Header;
uint16 unit_type;
uint16 grid_x;
uint16 grid_y;
}
entity_id Sender’s team slot (node address 0 - 3).
TODO
Packet 15
Not implemented in M.A.X. v1.04.
Packet 16 - Save Game
IPX multicast message sent by the player that issued a save operation to all other players.
struct MAX_Packet_16
{
MAX_PacketHeader Header;
string(30) file_name;
string(30) title;
}
entity_id Hardcoded to 0 by the sender and the field value is not used by the receivers.
file_name Name of the save file to be created on the file system. The field is always 30 characters long.
title Title of the save game set via the game GUI. The field is always 30 characters long.
Packet 17 - Update Game Settings
IPX multicast message sent by the host to each client right after Packet 13.
struct MAX_Packet_17
{
MAX_PacketHeader Header;
uint8 game_state;
uint8 world;
uint8 game_file_number;
uint8 game_file_type;
uint8 play_mode;
uint8 all_visible;
uint8 quick_build;
uint8 real_time;
uint8 log_file_debug;
uint8 disable_fire;
uint8 fast_movement;
uint16 timer;
uint16 endturn;
uint16 start_gold;
uint16 autosave;
uint8 victory_type;
uint16 victory_limit;
uint16 raw_normal_low;
uint16 raw_normal_high;
uint16 raw_concentrate_low;
uint16 raw_concentrate_high;
uint16 raw_concentrate_seperation;
uint16 raw_concentrate_diffusion;
uint16 fuel_normal_low;
uint16 fuel_normal_high;
uint16 fuel_concentrate_low;
uint16 fuel_concentrate_high;
uint16 fuel_concentrate_seperation;
uint16 fuel_concentrate_diffusion;
uint16 gold_normal_low;
uint16 gold_normal_high;
uint16 gold_concentrate_low;
uint16 gold_concentrate_high;
uint16 gold_concentrate_seperation;
uint16 gold_concentrate_diffusion;
uint16 mixed_resource_seperation;
uint16 min_resources;
uint16 max_resources;
uint16 alien_seperation;
uint16 alien_unit_value;
}
entity_id Hardcoded to 0 by the sender and the field value is not used by the receivers.
TODO: Describe all fields.
Packet 18 - In-game Chat Message
IPX unicast message sent by a player to another player. The message content is an in-game chat message. If an in-game chat message needs to be sent to multiple players than simply multiple packets are sent to the applicable IPX local address.
struct MAX_Packet_18
{
MAX_PacketHeader Header;
string(data_size) chat_message;
}
entity_id Sender’s team slot (node address 0 - 3).
chat_message Null terminated string. The packet size depends on the length of the chat message string. The sender’s name is prefixed by the receiving client before the message is shown to the given player.
On the sender side the chat message is limited to 550 characters including the null character at the end. On the receiver side the client statically reserves an 550 bytes long buffer for the message but it additionally prefixes the received chat message with a maximum 30 characters long player name and a colon plus a space character. So if a player would really be able to send such a long chat message it would corrupt all receiving player’s IPX network manager state data even certain callback functions could be overwritten so it would be possible to execute hostile code on the remote computer. The in-game chat GUI only allows 60 characters to be sent though.
Packet 19
Not implemented in M.A.X. v1.04.
Packet 20
struct MAX_Packet_20
{
MAX_PacketHeader Header;
uint16 unit_type;
uint16 turns;
uint16 hits;
uint16 armor;
uint16 attack;
uint16 speed;
uint16 range;
uint16 rounds;
uint16 move_and_fire;
uint16 scan;
uint16 storage;
uint16 ammo;
uint16 attack_radius;
uint16 agent_adjust;
}
Packet 21
struct MAX_Packet_21
{
MAX_PacketHeader Header;
uint16 complex_id;
uint16 material;
uint16 fuel;
uint16 gold;
}
Packet 22
struct MAX_Packet_22
{
MAX_PacketHeader Header;
uint16 team;
uint8 state;
uint8 repeat_build;
uint16 build_time;
uint16 build_rate;
uint16 target_grid_x;
uint16 target_grid_y;
uint16 count;
MAX_UnitType[*] buildings;
}
Packet 23 - Unit State
IPX multicast message sent by host to another players. The message needs to be acknowledged by each player with Packet 24.
Packet 23, Packet 24 and Packet 49 are only used for lockstep desynchronization analysis, for debugging purposes.
struct MAX_Packet_23
{
MAX_PacketHeader Header;
MAX_Orders orders;
uint8 state;
MAX_Orders prior_orders;
uint8 prior_state;
uint8 disabled_reaction_fire;
uint16 parent_unit_id;
uint16 target_grid_x;
uint16 target_grid_y;
uint16 enemy_unit_id;
uint8 total_mining;
uint8 raw_mining;
uint8 fuel_mining;
uint8 gold_mining;
uint16 build_time;
uint16 build_rate;
MAX_UnitType unit_type;
uint16 unit_id;
uint16 grid_x;
uint16 grid_y;
uint16 team;
uint16 hits;
uint16 speed;
uint16 shots;
uint16 storage;
uint16 ammo;
}
entity_id The unit’s UnitHash type hash key.
Packet 24 - Acknowledge Unit State Message
IPX multicast message sent by client to another players. The message acknowledges the reception of Packet 23.
struct MAX_Packet_24
{
MAX_PacketHeader Header;
uint8 status;
}
entity_id Sender’s team slot (node address 0 - 3).
status Hardcoded to 1 (true) by the sender.
Packet 25 - Remote Debug Log
Not implemented in M.A.X. v1.04.
Packet 26 - Set Team Clan
IPX multicast message sent by player to each other player after the host sent Packet 17.
struct MAX_Packet_26
{
MAX_PacketHeader Header;
uint8 team_clan;
}
entity_id Sender’s team slot (node address 0 - 3).
team_clan Clan index. Valid value range is 0 - 7.
Packet 27
Not implemented in M.A.X. v1.04.
Packet 28 - Join Request
IPX broadcast message sent by client that wants to join a game. The packet is only sent by a client if the host and its node address is not yet known. E.g. if the client did not receive any Packet 44 messages yet.
struct MAX_Packet_28
{
MAX_PacketHeader Header;
uint16 source_node;
raw(4) data;
}
entity_id Hardcoded to 0 by the sender and the field value is not used by the receivers.
source_node Node address of the client. As the host is not known yet the client’s node address is also not known yet so the field is set to 0.
data See Packet 31.
Packet 29
IPX broadcast message sent by client to host indicating that client wants to join the game that host is setting up. The host is identified via its Packet 44 message contents.
alias MAX_IpxAddress uint8[6];
struct MAX_Packet_29
{
MAX_PacketHeader Header;
MAX_IpxAddress address;
}
entity_id The host’s node address.
address On MS-DOS systems this is the real IPX address of the network interface. In DOSBox the first 4 bytes is the IPv4 address, the last two bytes are the UDP source port.
Packet 30
IPX unicast message sent to client from host as response to Packet 29. The packet synchronizes all game setup settings with the client.
struct MAX_Packet_30
{
MAX_PacketHeader Header;
uint16 player_node;
string(30)[4] team_name;
uint16[4] node;
string(30) world_name;
uint8 ini_world_index;
uint8 ini_play_mode;
uint16 ini_timer;
uint16 ini_endturn;
uint8 ini_opponent;
uint16 ini_start_gold;
uint8 ini_raw_resource;
uint8 ini_fuel_resource;
uint8 ini_gold_resource;
uint8 ini_alien_derelicts;
uint8 ini_victory_type;
uint16 ini_victory_limit;
uint8 is_map_changed;
uint8 is_multi_scenario;
string(30)[4] default_team_name;
uint8 multi_scenario_id;
uint32 rng_seed;
}
entity_id The client’s new node address assigned by the host.
player_node Node address of the host. The client saves this node address to be able to address the host directly.
team_name [4] Name of each known player. The field is always 4x 30 characters long.
node [4] Node address of each known player. Note that the client does not get its new node address from the host this way. The packet’s entity_id field is used for that purpose. Normally the host does not yet know which team slot will be requested by the newly registered client via Packet 35.
TODO: Describe all fields.
Packet 31 - Leave Game
IPX broadcast message sent by a player during game setup phase to denounce itself.
struct MAX_Packet_31
{
MAX_PacketHeader Header;
uint16 field_0;
raw(4) data;
}
entity_id The host’s node address.
field_0 Hardcoded to 0 by the sender and the field value is not used by the receivers.
data The packet size is always 6 bytes, but the last 4 bytes (this field) are not set. Unset random buffer content is sent by the player.
Packet 32 - Start Game
IPX broadcast message sent by the host to start the configured network game. Each client sends Packet 05 as response to the host.
struct MAX_Packet_32
{
MAX_PacketHeader Header;
MAX_TeamType team;
uint16 session_id;
MAX_IpxAddress address;
}
entity_id The host’s node address.
team Team slot of the sender (host).
session_id A newly generated random number in the range 0 to 32767. Starting from the next packet the Session ID field inside the IPX Protocol Header will be set to this field’s value for every node participating in the communication.
address Local IPX address of the host. See Packet 29.
Packet 33 - Chat Message
IPX broadcast message sent by the player that wants to send a chat message.
struct MAX_Packet_33
{
MAX_PacketHeader Header;
string(data_size) message;
}
entity_id The host’s node address. All peers registered for the given host’s game receives the message.
message Null terminated string. The field is always 120 bytes long. The message is prefixed with the sender’s team name (“Red Team: Hello M.A.X. Commander Green Team!”). In theory this means that the message contents could be 87 (120 - 1 - 30 -2) bytes long maximum. It is not possible to send a message to a single player only.
Packet 34 - Announce IPX Addresses
IPX multicast message sent by the host to all clients to acknowledge Packet 05 and to announce all players’ IPX local address.
Starting from this host message each player needs to increment their Packet Number field inside the IPX Protocol Header. The host keeps track of its packet counter for each player individually. Clients just increment their counter whenever they send a packet. This packet starts with Packet Number value 0.
struct MAX_Packet_34
{
MAX_PacketHeader Header;
MAX_IpxAddress[4] address;
}
entity_id The host’s node address.
address [4] Local IPX addresses of each player. See Packet 29.
Packet 35 - Change Team Slot
IPX broadcast message sent by a player that selected a new team slot (red, green, blue, gray).
The host always sends this message at least once after it sent the first Packet 44 announcement message.
Joining clients send this message as response to Packet 30.
struct MAX_Packet_35
{
MAX_PacketHeader Header;
uint16 source_node;
uint16 team_slot;
uint8 ready_state;
string(30) team_name;
}
entity_id The host’s node address.
source_node Node address of player that changed team slot.
team_slot The team slot of the player. Valid value range is 0 - 3. The value is used as offset into various network game menu manager object fields so a malformed field could corrupt quite a lot of things considering that the team_slot field is 16 bits.
ready_state 0 (false) if the client is not ready to start the game. 1 (true) if the client is ready. If a client changed to ready state the only way to unready itself is to push the Cancel button which also denounces the player (see Packet 31). The host can only start the actual game when each joined client sets this field to 1.
team_name The player (team) name as a null terminated string. The field is always 30 characters long. The game uses strcpy() to store the field for which a 30 bytes buffer is reserved so the name should not be longer than 29 characters. If the packet is malformed the network game menu manager object could be corrupted. The game does not allow an empty string as the team_name. If the player attempts to set such a name via the GUI the resulting name will be “No Name”. So a value of 0x00 for the team_name field is also malformed.
M.A.X. v1.04 implements an alternative Packet 35 format too, but it is unused. Probably dead code.
Packet 36 - Change Player Name
IPX broadcast message sent by a player that changed their name.
struct MAX_Packet_36
{
MAX_PacketHeader Header;
string(30) team_name;
}
entity_id Sender’s team slot (node address 0 - 3).
team_name See Packet 35 and Packet 44. Note that players could have identical names which could be confusing as chat messages are prefixed with the team names only.
Packet 37 - Change Game Options
IPX broadcast message sent by host when map is changed, a previous network game is loaded, a multiplayer scenario is selected or game options like starting credit is changed.
enum8 MAX_WorldIndex
{
SNOWCRAB 0
FRIGIA 1
ICE_BERG 2
THE_COOLER 3
ULTIMA_THULE 4
LONG_FLOES 5
IRON_CROSS 6
SPLATTERSCAPE 7
PEAK-A-BOO 8
VALENTINES_PLANET 9
THREE_RINGS 10
GREAT_DIVIDE 11
NEW_LUZON 12
MIDDLE_SEA 13
HIGH_IMPACT 14
SANCTUARY 15
ISLANDIA 16
HAMMERHEAD 17
FRECKLES 18
SANDSPIT 19
GREAT_CIRCLE 20
LONG_PASSAGE 21
FLASH_POINT 22
BOTTLENECK 23
}
struct MAX_Packet_37
{
MAX_PacketHeader Header;
string(30) world_name;
MAX_WorldIndex ini_world_index;
uint8 ini_play_mode;
uint16 ini_timer;
uint16 ini_endturn;
uint8 ini_opponent;
uint16 ini_start_gold;
uint8 ini_raw_resource;
uint8 ini_fuel_resource;
uint8 ini_gold_resource;
uint8 ini_alien_derelicts;
uint8 ini_victory_type;
uint16 ini_victory_limit;
uint8 is_map_changed;
uint8 is_multi_scenario;
string(30)[4] default_team_name;
uint8 multi_scenario_id;
uint32 rng_seed;
}
entity_id The host’s node address.
world_name Name of the selected map or multiplayer scenario or save slot. The field is always 30 bytes long. The field is a Null terminated string.
ini_world_index The index of the map. The game supports 24 worlds, 6 worlds in 4 galaxies.
TODO: Describe all fields.
Packet 38 - Unit Path
IPX multicast message sent by player to all other players when unit path is changed. The UnitPath (or derived) path object of the given unit is replaced with a new object constructed from the data found in this packet. The packet is sent usually right after Packet 08.
struct MAX_PathStep
{
byte_order little_endian;
int8 x;
int8 y;
}
struct MAX_Packet_38
{
MAX_PacketHeader Header;
MAX_Orders order;
uint8 state;
uint16 target_grid_x;
uint16 target_grid_y;
uint16 end_x;
uint16 end_y;
uint32 distance_x;
uint32 distance_y;
uint16 euclidean_distance;
uint8 field_79;
uint8 speed;
uint8 move_fraction;
uint8 max_velocity;
uint16 list_size;
MAX_PathStep[list_size] steps;
}
entity_id The unit’s UnitHash type hash key.
order Current unit order. See Packet 08.
state State variable of the current order. TODO: Figure out states.
target_grid_x X coordinate of the calculated path’s end point on the grid. Note that the game GUI shows grid position +1.
target_grid_x Y coordinate of the calculated path’s end point on the grid. Note that the game GUI shows grid position +1.
end_x
end_y
distance_x
distance_y
euclidean_distance
field_79
speed
move_fraction
max_velocity
list_size Number of path steps that follows.
steps Each element represents an 8-direction path step. E.g. {-1,0} means take one step to the left. Units not looking in the given direction need to turn.
TODO: Describe all fields.
Packet 39 - Pause Game
IPX multicast message sent by player to all other players to pause the game. See Packet 40 to continue the game.
struct MAX_Packet_39
{
MAX_PacketHeader Header;
uint8 field_0;
}
entity_id Sender’s team slot (node address 0 - 3).
field_0 Always set to 0. Note that the field is not used for anything, it is basically reserved.
Packet 40 - Unpause Game
IPX multicast message sent by each player to all other players to unpause the game.
struct MAX_Packet_40
{
MAX_PacketHeader Header;
uint8 field_0;
}
entity_id Sender’s team slot (node address 0 - 3).
field_0 Always set to 0. Note that the field is not used for anything, it is basically reserved.
Packet 41 - Path Blocked
IPX multicast message sent by player to all other players that owns the blocked unit.
In M.A.X. v1.04 the function which builds packet 41 forgets to set the packet header’s data_size field so random garbage in random size is sent over the network. Typically a single byte of data is sent as the most common packet on the network has one data byte.
struct MAX_Packet_41
{
MAX_PacketHeader Header;
raw(*) data;
}
entity_id The unit’s UnitHash type hash key.
Packet 42 -
IPX multicast message. TODO
struct MAX_Packet_42
{
MAX_PacketHeader Header;
uint8 field_0;
}
entity_id Sender’s team slot (node address 0 - 3).
field_0 Always set to 0. Note that the field is not used for anything, it is basically reserved.
Packet 43 - Rename Unit
IPX multicast message sent by player to all other players when unit name has changed.
struct MAX_Packet_43
{
MAX_PacketHeader Header;
string(data_size) unit_name;
}
entity_id The unit’s UnitHash type hash key.
unit_name New name of the unit. Null terminated string. The maximum length of the name is 30 bytes including the null character. The game GUI does not allow longer names to be set. A longer (malformed) packet does not corrupt memory as the name is stored in heap memory. A malformed packet where the null character is missing could potentially lead to segmentation fault or other issues still.
Packet 44 - Announce Host
IPX broadcast message sent by host every 3 seconds or so during game setup phase to announce itself.
struct MAX_Packet_44
{
MAX_PacketHeader Header;
string(data_size-30) version_string;
string(30) team_name;
}
entity_id The host’s node address.
version_string The value in M.A.X. v1.04 is “v1.04” (5 bytes) without the quotes. The version string is not null terminated.
team_name The host player’s name like “Red Team” or “Player 1” or “Mech Commander - Carlton L.”. The field is always 30 characters long and it is processed as a null terminated string. Therefore at least the last byte shall be 0x00.
Packet 45 -
IPX multicast message sent by player that started their turn to the others. The other players respond with their own calculation results for the sender specific local data. If any player’s calculation of the current player’s data does not match a network desynchronization error is reported. The game cannot be continued in such cases, but the last backup save could be reloaded or a restart sequence could be attempted.
struct MAX_Packet_45
{
MAX_PacketHeader Header;
uint8 next_turn;
uint16{d=hex} checksum;
}
entity_id Sender’s team slot (node address 0 - 3).
next_turn Current turn index plus one. See Packet 52.
checksum A 16 bit special CRC checksum calculated over specific unit lists. The algorithm is simple, but the data fed to the algorithm is complex and numerous. As xor
operation is involved in the algorithm the order of data fed to the algorithm matters.
Initial CRC value is 0xFFFF.
unsigned short crc16(unsigned short crc, unsigned short data) {
for (int i = 0; i < 16; ++i) {
if (data & 0x8000) {
data *= 2;
data ^= 0x1021;
} else {
data *= 2;
}
}
return crc ^ data;
}
Packet 46 -
IPX multicast message. TODO
struct MAX_Packet_46
{
MAX_PacketHeader Header;
uint8 field_0;
uint32 field_1;
}
entity_id Sender’s team slot (node address 0 - 3).
TODO
Packet 47
Not implemented in M.A.X. v1.04.
Packet 48 - Acknowledge Exit Game
IPX multicast message sent by player to each other player after receiving Packet 07.
struct MAX_Packet_48
{
MAX_PacketHeader Header;
uint8 request_uid;
}
entity_id Sender’s team slot (node address 0 - 3).
request_uid Unique request identifier from previously received Packet 07.
Packet 49 - Conclude Analysis
IPX multicast message sent by host to each players to conclude a lockstep desynchronization analysis process. See Packet 23.
struct MAX_Packet_49
{
MAX_PacketHeader Header;
uint8 status;
}
entity_id Hardcoded to 0 by the sender and the field value is not used by the receivers.
status Hardcoded to 1 (true) by the sender.
Packet 50 - Enemy Spotted
IPX multicast message sent by player to all other players.
struct MAX_Packet_50
{
MAX_PacketHeader Header;
}
entity_id The unit’s UnitHash type hash key.
Packet 51 - Restart After Desync
IPX multicast message sent by player that selected option to restart from last save after desync to all other players.
struct MAX_Packet_51
{
MAX_PacketHeader Header;
uint8 field_0;
}
entity_id Sender’s team slot (node address 0 - 3).
field_0 Always set to 0. Note that the field is not used for anything, it is basically reserved.
Packet 52 - Acknowledge End Turn
IPX multicast message sent by player to each other player after receiving Packet 06.
struct MAX_Packet_52
{
MAX_PacketHeader Header;
uint8 turn_index;
}
entity_id Sender’s team slot (node address 0 - 3).
turn_index Current turn counter value. Each player’s turn increments this field. The turn counter displayed in-game is not equal to this counter value in turn based games.
Packet 255
IPX unicast message. This is an Internet or Network layer packet used by the IPX driver to request (re)transmission of the next packet that the receiver node expects to read in case it is not yet found in the IPX driver’s Rx ring buffer. The data_size field in the header is 0 as there is no data associated with this packet type.
If network conditions are good, then this packet type does not appear in the communication flow.
struct MAX_Packet_255
{
MAX_PacketHeader Header;
}
entity_id The field is set to the Rx packet index, Packet Number field of the IPX Protocol Header, that is requested by the receiver node that sends the request.
References
[1] DOSBox Connectivity
[2] Age of Empires Multiplayer Design
[3] Internetwork Packet Exchange - October 2001