1
0

Compare commits

...

2 Commits

Author SHA1 Message Date
ebeae76cb0 add readme 2025-09-08 13:48:51 -05:00
2cc96422a2 packet generation experiment 2025-09-08 13:46:30 -05:00
6 changed files with 349 additions and 40 deletions

12
README.md Normal file
View File

@ -0,0 +1,12 @@
# Game
This is a little test bench for a multiplayer game. Not sure if it'll scale to MMO, but it's a great
practice run for learning how to build one. Uses C++ with Raylib for the client, and Go for the server.
## Client
A dead-simple client using C++ with Raylib. The goal is to compile it for Windows, Linux, and macOS.
## Server
Bare-bones UDP server using Go. Since speed is a primary concern, we'll likely leverage gnet at some point.

View File

@ -20,4 +20,7 @@ clean:
run: $(TARGET) run: $(TARGET)
./$(TARGET) ./$(TARGET)
.PHONY: all clean run packets:
cd .. && lua generate_packets.lua
.PHONY: all clean run packets

198
generate_packets.php Executable file
View File

@ -0,0 +1,198 @@
#!/usr/bin/env php
<?php
/**
* Generate packet definitions for C++ client and Go server from packets.json
*/
/**
* Load packet definitions from JSON file
*/
function load_packet_definitions($filename = 'packets.json') {
if (!file_exists($filename)) {
throw new Exception("Could not open $filename");
}
$content = file_get_contents($filename);
$packets = json_decode($content, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("JSON decode error: " . json_last_error_msg());
}
return $packets;
}
/**
* Convert PascalCase to SNAKE_CASE
*/
function to_snake_case($str) {
$result = preg_replace('/([A-Z])/', '_$1', $str);
return strtoupper(ltrim($result, '_'));
}
/**
* Generate C++ header file with packet definitions
*/
function generate_cpp_header($packets) {
$header = <<<CPP
// Auto-generated packet definitions from packets.json
// DO NOT EDIT MANUALLY
#pragma once
#include <cstdint>
#include <string>
CPP;
// Generate enum
$header .= "enum class MessageType : uint8_t {\n";
// Sort keys for consistent output
$names = array_keys($packets['opcodes']);
sort($names);
foreach ($names as $i => $name) {
$packet = $packets['opcodes'][$name];
$header .= sprintf(" %s = %s", $name, $packet['id']);
if ($i < count($names) - 1) {
$header .= ",";
}
$header .= "\n";
}
$header .= "};\n\n";
// Generate packet structures
$header .= "// Packet structure definitions\n";
$header .= "namespace Packets {\n\n";
foreach ($names as $name) {
$packet = $packets['opcodes'][$name];
if (!empty($packet['fields'])) {
$header .= sprintf("struct %s {\n", $name);
$header .= sprintf(" static constexpr MessageType TYPE = MessageType::%s;\n", $name);
foreach ($packet['fields'] as $field) {
$cpp_type = '';
switch ($field['type']) {
case 'string':
$cpp_type = 'std::string';
break;
case 'uint8':
$cpp_type = 'uint8_t';
break;
case 'uint32':
$cpp_type = 'uint32_t';
break;
case 'float32':
$cpp_type = 'float';
break;
case 'array':
$header .= sprintf(" // Array field: %s - requires custom handling\n", $field['name']);
continue 2;
}
$header .= sprintf(" %s %s;\n", $cpp_type, $field['name']);
}
$header .= "};\n\n";
}
}
$header .= "} // namespace Packets\n";
return $header;
}
/**
* Generate Go file with packet definitions
*/
function generate_go_file($packets) {
$go_file = <<<GO
// Auto-generated packet definitions from packets.json
// DO NOT EDIT MANUALLY
package main
// Message type constants
const (
GO;
$go_file .= "\n";
// Sort keys for consistent output
$names = array_keys($packets['opcodes']);
sort($names);
// Generate constants
foreach ($names as $name) {
$packet = $packets['opcodes'][$name];
$const_name = 'MSG_' . to_snake_case($name);
$go_file .= sprintf("\t%s = %s\n", $const_name, $packet['id']);
}
$go_file .= ")\n\n";
// Generate packet structures
$go_file .= "// Packet structure definitions\n\n";
foreach ($names as $name) {
$packet = $packets['opcodes'][$name];
if (!empty($packet['fields'])) {
$struct_name = "Packet $name";
$go_file .= sprintf("type %s struct {\n", $struct_name);
foreach ($packet['fields'] as $field) {
$field_name = ucfirst($field['name']);
$go_type = '';
switch ($field['type']) {
case 'string':
$go_type = 'string';
break;
case 'uint8':
$go_type = 'uint8';
break;
case 'uint32':
$go_type = 'uint32';
break;
case 'float32':
$go_type = 'float32';
break;
case 'array':
$go_file .= sprintf("\t// Array field: %s - requires custom handling\n", $field_name);
continue 2;
}
$go_file .= sprintf("\t%s %s\n", $field_name, $go_type);
}
$go_file .= "}\n\n";
}
}
return $go_file;
}
try {
// Load packet definitions
$packets = load_packet_definitions();
// Generate C++ header
$cpp_header = generate_cpp_header($packets);
$cpp_path = 'client/net/PacketDefinitions.hpp';
file_put_contents($cpp_path, $cpp_header);
echo "Generated $cpp_path\n";
// Generate Go file
$go_file = generate_go_file($packets);
$go_path = 'server/packet_definitions.go';
file_put_contents($go_path, $go_file);
echo "Generated $go_path\n";
echo sprintf("Successfully generated packet definitions for protocol version %s\n", $packets['version']);
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
exit(1);
}

96
packets.json Normal file
View File

@ -0,0 +1,96 @@
{
"version": "1.0.0",
"opcodes": {
"Login": {
"id": "0x01",
"fields": []
},
"Position": {
"id": "0x02",
"fields": [
{"name": "x", "type": "float32"},
{"name": "y", "type": "float32"},
{"name": "z", "type": "float32"}
]
},
"Spawn": {
"id": "0x03",
"fields": [
{"name": "playerId", "type": "uint32"},
{"name": "x", "type": "float32"},
{"name": "y", "type": "float32"},
{"name": "z", "type": "float32"},
{"name": "color", "type": "string"}
]
},
"Move": {
"id": "0x04",
"fields": [
{"name": "playerId", "type": "uint32"},
{"name": "dx", "type": "float32"},
{"name": "dy", "type": "float32"},
{"name": "dz", "type": "float32"}
]
},
"Update": {
"id": "0x05",
"fields": [
{"name": "playerId", "type": "uint32"},
{"name": "x", "type": "float32"},
{"name": "y", "type": "float32"},
{"name": "z", "type": "float32"}
]
},
"PlayerJoined": {
"id": "0x06",
"fields": [
{"name": "playerId", "type": "uint32"},
{"name": "x", "type": "float32"},
{"name": "y", "type": "float32"},
{"name": "z", "type": "float32"},
{"name": "color", "type": "string"}
]
},
"PlayerLeft": {
"id": "0x07",
"fields": [
{"name": "playerId", "type": "uint32"}
]
},
"PlayerList": {
"id": "0x08",
"fields": [
{"name": "count", "type": "uint8"},
{"name": "players", "type": "array", "itemType": {
"fields": [
{"name": "playerId", "type": "uint32"},
{"name": "x", "type": "float32"},
{"name": "y", "type": "float32"},
{"name": "z", "type": "float32"},
{"name": "color", "type": "string"}
]
}}
]
},
"ChangeColor": {
"id": "0x09",
"fields": [
{"name": "playerId", "type": "uint32"},
{"name": "color", "type": "string"}
]
},
"ColorChanged": {
"id": "0x0A",
"fields": [
{"name": "playerId", "type": "uint32"},
{"name": "color", "type": "string"}
]
}
},
"types": {
"uint8": {"size": 1, "encoding": "little-endian"},
"uint32": {"size": 4, "encoding": "little-endian"},
"float32": {"size": 4, "encoding": "little-endian"},
"string": {"encoding": "length-prefixed", "lengthType": "uint8"}
}
}