0
0
mirror of https://github.com/pmmp/BedrockProtocol.git synced 2024-11-23 13:46:18 +00:00
BedrockProtocol/tools/generate-protocol-info.php
Dylan K. Taylor 68d818091f
Ported protocol_info_generator codegen stuff to PHP
this now takes a JSON file as input, as provided by protocol_info_dumper.py in bds-modding-devkit.
2024-10-26 17:56:04 +01:00

468 lines
14 KiB
PHP

<?php
/*
* This file is part of BedrockProtocol.
* Copyright (C) 2014-2022 PocketMine Team <https://github.com/pmmp/BedrockProtocol>
*
* BedrockProtocol is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\protocol\tools\generate_protocol_info;
use pocketmine\network\mcpe\protocol\ClientboundPacket;
use pocketmine\network\mcpe\protocol\DataPacket;
use pocketmine\network\mcpe\protocol\Packet;
use pocketmine\network\mcpe\protocol\PacketDecodeException;
use pocketmine\network\mcpe\protocol\PacketHandlerDefaultImplTrait;
use pocketmine\network\mcpe\protocol\PacketHandlerInterface;
use pocketmine\network\mcpe\protocol\PacketPool;
use pocketmine\network\mcpe\protocol\ServerboundPacket;
use function array_fill_keys;
use function asort;
use function ceil;
use function count;
use function dechex;
use function dirname;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function fwrite;
use function implode;
use function is_array;
use function is_bool;
use function is_int;
use function is_string;
use function json_decode;
use function max;
use function preg_split;
use function scandir;
use function sprintf;
use function str_contains;
use function str_ends_with;
use function str_pad;
use function strlen;
use function strtoupper;
use function substr;
use const DIRECTORY_SEPARATOR;
use const JSON_THROW_ON_ERROR;
use const PHP_EOL;
use const PREG_SPLIT_DELIM_CAPTURE;
use const PREG_SPLIT_NO_EMPTY;
use const SORT_NUMERIC;
use const STDERR;
use const STR_PAD_LEFT;
const DATA_PACKET_TEMPLATE = <<<'CODE'
<?php
/*
* This file is part of BedrockProtocol.
* Copyright (C) 2014-2022 PocketMine Team <https://github.com/pmmp/BedrockProtocol>
*
* BedrockProtocol is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\protocol;
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
class %s extends DataPacket{
public const NETWORK_ID = ProtocolInfo::%s;
/**
* @generate-create-func
*/
public static function create() : self{
$result = new self;
//TODO: add fields
return $result;
}
protected function decodePayload(PacketSerializer $in) : void{
//TODO
}
protected function encodePayload(PacketSerializer $out) : void{
//TODO
}
public function handle(PacketHandlerInterface $handler) : bool{
return $handler->handle%s($this);
}
}
CODE;
const PACKET_HANDLER_TRAIT_TEMPLATE = <<<'CODE'
<?php
/*
* This file is part of BedrockProtocol.
* Copyright (C) 2014-2022 PocketMine Team <https://github.com/pmmp/BedrockProtocol>
*
* BedrockProtocol is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\protocol;
/**
* This trait provides default implementations for all packet handlers, so that you don't have to manually write
* handlers for every single packet even if you don't care about them.
*
* This file is automatically generated. Do not edit it manually.
*/
trait PacketHandlerDefaultImplTrait{
%s
}
CODE;
const PACKET_HANDLER_INTERFACE_TEMPLATE = <<<'CODE'
<?php
/*
* This file is part of BedrockProtocol.
* Copyright (C) 2014-2022 PocketMine Team <https://github.com/pmmp/BedrockProtocol>
*
* BedrockProtocol is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\protocol;
/**
* This class is an automatically generated stub. Do not edit it manually.
*/
interface PacketHandlerInterface{
%s
}
CODE;
const PACKET_POOL_TEMPLATE = <<<'CODE'
<?php
/*
* This file is part of BedrockProtocol.
* Copyright (C) 2014-2022 PocketMine Team <https://github.com/pmmp/BedrockProtocol>
*
* BedrockProtocol is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\protocol;
use pocketmine\utils\Binary;
use pocketmine\utils\BinaryDataException;
class PacketPool{
protected static ?PacketPool $instance = null;
public static function getInstance() : self{
if(self::$instance === null){
self::$instance = new self;
}
return self::$instance;
}
/** @var \SplFixedArray<Packet> */
protected \SplFixedArray $pool;
public function __construct(){
$this->pool = new \SplFixedArray(%d);
%s
}
public function registerPacket(Packet $packet) : void{
$this->pool[$packet->pid()] = clone $packet;
}
public function getPacketById(int $pid) : ?Packet{
return isset($this->pool[$pid]) ? clone $this->pool[$pid] : null;
}
/**
* @throws BinaryDataException
*/
public function getPacket(string $buffer) : ?Packet{
$offset = 0;
return $this->getPacketById(Binary::readUnsignedVarInt($buffer, $offset) & DataPacket::PID_MASK);
}
}
CODE;
const PROTOCOL_INFO_TEMPLATE = <<<'CODE'
<?php
/*
* This file is part of BedrockProtocol.
* Copyright (C) 2014-2022 PocketMine Team <https://github.com/pmmp/BedrockProtocol>
*
* BedrockProtocol is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/
declare(strict_types=1);
namespace pocketmine\network\mcpe\protocol;
/**
* Version numbers and packet IDs for the current Minecraft PE protocol
*/
final class ProtocolInfo{
private function __construct(){
//NOOP
}
/**
* NOTE TO DEVELOPERS
* Do not waste your time or ours submitting pull requests changing game and/or protocol version numbers.
* Pull requests changing game and/or protocol version numbers will be closed.
*
* This file is generated automatically, do not edit it manually.
*/
/** Actual Minecraft: PE protocol version */
public const CURRENT_PROTOCOL = %d;
/** Current Minecraft PE version reported by the server. This is usually the earliest currently supported version. */
public const MINECRAFT_VERSION = '%s';
/** Version number sent to clients in ping responses. */
public const MINECRAFT_VERSION_NETWORK = '%s';
%s
}
CODE;
/**
* @return string[]
*/
function split_upper(string $string) : array{
$split = preg_split('/([A-Z][^A-Z]*)/', $string, flags: PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
if($split === false){
throw new \Error("preg_split failed");
}
return $split;
}
function rchop(string $string, string $substring) : string{
if(str_ends_with($string, $substring)){
return substr($string, 0, -strlen($substring));
}
return $string;
}
/**
* @param string[] $packetToIdList
* @phpstan-param array<string, int> $packetToIdList
*/
function generate_new_packet_stubs(array $packetToIdList, string $packetsDir) : void{
foreach($packetToIdList as $name => $id){
$packetFilePath = $packetsDir . DIRECTORY_SEPARATOR . $name . '.php';
if(!file_exists($packetFilePath)){
echo "!!! New packet: $name" . PHP_EOL;
$constName = strtoupper(implode('_', split_upper($name)));
$baseName = rchop($name, 'Packet');
file_put_contents($packetFilePath, sprintf(DATA_PACKET_TEMPLATE, $name, $constName, $baseName));
echo "Created stub class for $name at $packetFilePath" . PHP_EOL;
}
}
}
/**
* @param string[] $packetToIdList
* @phpstan-param array<string, int> $packetToIdList
*/
function check_removed_packets(array $packetToIdList, string $packetsDir) : void{
$existing = scandir($packetsDir);
if($existing === false){
return;
}
//use ::class constants here so that they are checked for existence
$ignoredClasses = array_fill_keys([
DataPacket::class,
PacketPool::class,
Packet::class,
PacketDecodeException::class,
PacketHandlerDefaultImplTrait::class,
PacketHandlerInterface::class,
ClientboundPacket::class,
ServerboundPacket::class,
], true);
foreach($existing as $fileName){
if(str_ends_with($fileName, ".php")){
$packetName = substr($fileName, 0, -strlen(".php"));
if(!str_contains($packetName, "Packet") || isset($ignoredClasses["pocketmine\\network\\mcpe\\protocol\\" . $packetName])){
continue;
}
if(!isset($packetToIdList[$packetName])){
echo "!!! Removed packet: $packetName" . PHP_EOL;
}
}
}
}
/**
* @param string[] $packetToIdList
* @phpstan-param array<string, int> $packetToIdList
*/
function generate_protocol_info(array $packetToIdList, int $protocolVersion, int $major, int $minor, int $patch, int $revision, bool $beta, string $packetsDir) : void{
$consts = "";
$last = 0;
foreach($packetToIdList as $name => $id){
if($id !== $last + 1){
$consts .= "\n";
}
$last = $id;
$consts .= sprintf(
"\tpublic const %s = %s;\n",
strtoupper(implode("_", split_upper($name))),
"0x" . str_pad(dechex($id), 2, "0", STR_PAD_LEFT)
);
}
$gameVersion = sprintf("v%d.%d.%d%s", $major, $minor, $patch, $beta ? ".$revision beta" : "");
$gameVersionNetwork = sprintf("%d.%d.%d%s", $major, $minor, $patch, $beta ? ".$revision" : "");
file_put_contents($packetsDir . DIRECTORY_SEPARATOR . "ProtocolInfo.php", sprintf(
PROTOCOL_INFO_TEMPLATE,
$protocolVersion,
$gameVersion,
$gameVersionNetwork,
$consts
));
echo "Recreated ProtocolInfo" . PHP_EOL;
}
/**
* @param string[] $packetToIdList
* @phpstan-param array<string, int> $packetToIdList
*/
function generate_packet_pool(array $packetToIdList, string $packetsDir) : void{
$entries = "";
foreach($packetToIdList as $name => $id){
$entries .= sprintf("\n\t\t\$this->registerPacket(new %s());", $name);
}
$poolSize = (int) (ceil(max($packetToIdList) / 256) * 256);
file_put_contents($packetsDir . DIRECTORY_SEPARATOR . "PacketPool.php", sprintf(
PACKET_POOL_TEMPLATE,
$poolSize,
$entries
));
echo "Recreated PacketPool\n";
}
/**
* @param string[] $packetToIdList
* @phpstan-param array<string, int> $packetToIdList
*/
function generate_packet_handler_classes(array $packetToIdList, string $packetsDir) : void{
$interfaceFunctions = [];
$traitFunctions = [];
foreach($packetToIdList as $name => $id){
$baseName = rchop($name, "Packet");
$interfaceFunctions[] = sprintf("\tpublic function handle%s(%s \$packet) : bool;", $baseName, $name);
$traitFunctions[] = sprintf("\tpublic function handle%s(%s \$packet) : bool{\n\t\treturn false;\n\t}", $baseName, $name);
}
file_put_contents($packetsDir . DIRECTORY_SEPARATOR . "PacketHandlerInterface.php", sprintf(
PACKET_HANDLER_INTERFACE_TEMPLATE,
implode("\n\n", $interfaceFunctions)
));
echo "Recreated PacketHandlerInterface" . PHP_EOL;
file_put_contents($packetsDir . DIRECTORY_SEPARATOR . "PacketHandlerDefaultImplTrait.php", sprintf(
PACKET_HANDLER_TRAIT_TEMPLATE,
implode("\n\n", $traitFunctions)
));
echo "Recreated PacketHandlerDefaultImplTrait" . PHP_EOL;
}
if(count($argv) < 2){
fwrite(STDERR, "Please provide an input protocol_info.json file" . PHP_EOL);
exit(1);
}
$rawData = file_get_contents($argv[1]);
if($rawData === false){
fwrite(STDERR, "Couldn't read data from " . $argv[1] . PHP_EOL);
exit(1);
}
try{
$json = json_decode($rawData, associative: true, flags: JSON_THROW_ON_ERROR);
}catch(\JsonException $e){
fwrite(STDERR, "Error decoding input file: " . $e->getMessage() . PHP_EOL);
exit(1);
}
if(!is_array($json) || count($json) !== 2 || !is_array($json["version"] ?? null) || !is_array($json["packets"] ?? null)){
fwrite(STDERR, "Invalid input file, expected 2 objects: \"version\" and \"packets\"" . PHP_EOL);
exit(1);
}
$versionInfo = $json["version"];
$major = $versionInfo["major"] ?? null;
$minor = $versionInfo["minor"] ?? null;
$patch = $versionInfo["patch"] ?? null;
$revision = $versionInfo["revision"] ?? null;
$beta = $versionInfo["beta"] ?? null;
$protocolVersion = $versionInfo["protocol_version"] ?? null;
if(!is_int($major) || !is_int($minor) || !is_int($patch) || !is_int($revision) || !is_bool($beta) || !is_int($protocolVersion)){
fwrite(STDERR, "Invalid version info, expected \"major\" (int), \"minor\" (int), \"patch\" (int), \"revision\" (int), \"beta\" (bool) and \"protocol_version\" (int)" . PHP_EOL);
exit(1);
}
echo "Generating code basics for version $major.$minor.$patch.$revision " . ($beta ? "beta" : "") . " (protocol $protocolVersion)" . PHP_EOL;
$packetToIdList = [];
foreach($json["packets"] as $name => $id){
if(!is_string($name) || !is_int($id)){
fwrite(STDERR, "Invalid packet entry \"$name\", expected string => int" . PHP_EOL);
exit(1);
}
$packetToIdList[$name] = $id;
}
asort($packetToIdList, SORT_NUMERIC);
$packetsDir = dirname(__DIR__) . '/src/';
generate_protocol_info($packetToIdList, $protocolVersion, $major, $minor, $patch, $revision, $beta, $packetsDir);
generate_packet_pool($packetToIdList, $packetsDir);
generate_packet_handler_classes($packetToIdList, $packetsDir);
check_removed_packets($packetToIdList, $packetsDir);
generate_new_packet_stubs($packetToIdList, $packetsDir);
echo "Done" . PHP_EOL;