mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2024-11-11 11:59:21 +00:00
602 lines
22 KiB
PHP
602 lines
22 KiB
PHP
<?php
|
|
|
|
/*
|
|
*
|
|
* ____ _ _ __ __ _ __ __ ____
|
|
* | _ \ ___ ___| | _____| |_| \/ (_)_ __ ___ | \/ | _ \
|
|
* | |_) / _ \ / __| |/ / _ \ __| |\/| | | '_ \ / _ \_____| |\/| | |_) |
|
|
* | __/ (_) | (__| < __/ |_| | | | | | | | __/_____| | | | __/
|
|
* |_| \___/ \___|_|\_\___|\__|_| |_|_|_| |_|\___| |_| |_|_|
|
|
*
|
|
* This program 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.
|
|
*
|
|
* @author PocketMine Team
|
|
* @link http://www.pocketmine.net/
|
|
*
|
|
*
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace pocketmine\tools\generate_bedrock_data_from_packets;
|
|
|
|
use pocketmine\crafting\json\FurnaceRecipeData;
|
|
use pocketmine\crafting\json\ItemStackData;
|
|
use pocketmine\crafting\json\PotionContainerChangeRecipeData;
|
|
use pocketmine\crafting\json\PotionTypeRecipeData;
|
|
use pocketmine\crafting\json\RecipeIngredientData;
|
|
use pocketmine\crafting\json\ShapedRecipeData;
|
|
use pocketmine\crafting\json\ShapelessRecipeData;
|
|
use pocketmine\crafting\json\SmithingTransformRecipeData;
|
|
use pocketmine\crafting\json\SmithingTrimRecipeData;
|
|
use pocketmine\data\bedrock\block\BlockStateData;
|
|
use pocketmine\data\bedrock\item\BlockItemIdMap;
|
|
use pocketmine\data\bedrock\item\ItemTypeNames;
|
|
use pocketmine\nbt\LittleEndianNbtSerializer;
|
|
use pocketmine\nbt\NBT;
|
|
use pocketmine\nbt\tag\CompoundTag;
|
|
use pocketmine\nbt\tag\ListTag;
|
|
use pocketmine\nbt\TreeRoot;
|
|
use pocketmine\network\mcpe\convert\BlockStateDictionary;
|
|
use pocketmine\network\mcpe\convert\BlockTranslator;
|
|
use pocketmine\network\mcpe\convert\ItemTranslator;
|
|
use pocketmine\network\mcpe\handler\PacketHandler;
|
|
use pocketmine\network\mcpe\protocol\AvailableActorIdentifiersPacket;
|
|
use pocketmine\network\mcpe\protocol\BiomeDefinitionListPacket;
|
|
use pocketmine\network\mcpe\protocol\CraftingDataPacket;
|
|
use pocketmine\network\mcpe\protocol\CreativeContentPacket;
|
|
use pocketmine\network\mcpe\protocol\PacketPool;
|
|
use pocketmine\network\mcpe\protocol\serializer\ItemTypeDictionary;
|
|
use pocketmine\network\mcpe\protocol\serializer\PacketSerializer;
|
|
use pocketmine\network\mcpe\protocol\StartGamePacket;
|
|
use pocketmine\network\mcpe\protocol\types\CacheableNbt;
|
|
use pocketmine\network\mcpe\protocol\types\inventory\CreativeContentEntry;
|
|
use pocketmine\network\mcpe\protocol\types\inventory\ItemStack;
|
|
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackExtraData;
|
|
use pocketmine\network\mcpe\protocol\types\inventory\ItemStackExtraDataShield;
|
|
use pocketmine\network\mcpe\protocol\types\recipe\ComplexAliasItemDescriptor;
|
|
use pocketmine\network\mcpe\protocol\types\recipe\FurnaceRecipe;
|
|
use pocketmine\network\mcpe\protocol\types\recipe\IntIdMetaItemDescriptor;
|
|
use pocketmine\network\mcpe\protocol\types\recipe\MolangItemDescriptor;
|
|
use pocketmine\network\mcpe\protocol\types\recipe\MultiRecipe;
|
|
use pocketmine\network\mcpe\protocol\types\recipe\RecipeIngredient;
|
|
use pocketmine\network\mcpe\protocol\types\recipe\ShapedRecipe;
|
|
use pocketmine\network\mcpe\protocol\types\recipe\ShapelessRecipe;
|
|
use pocketmine\network\mcpe\protocol\types\recipe\SmithingTransformRecipe;
|
|
use pocketmine\network\mcpe\protocol\types\recipe\SmithingTrimRecipe;
|
|
use pocketmine\network\mcpe\protocol\types\recipe\StringIdMetaItemDescriptor;
|
|
use pocketmine\network\mcpe\protocol\types\recipe\TagItemDescriptor;
|
|
use pocketmine\network\PacketHandlingException;
|
|
use pocketmine\utils\AssumptionFailedError;
|
|
use pocketmine\utils\Filesystem;
|
|
use pocketmine\utils\Utils;
|
|
use pocketmine\world\format\io\GlobalBlockStateHandlers;
|
|
use Ramsey\Uuid\Exception\InvalidArgumentException;
|
|
use Symfony\Component\Filesystem\Path;
|
|
use function array_map;
|
|
use function array_values;
|
|
use function asort;
|
|
use function base64_decode;
|
|
use function base64_encode;
|
|
use function bin2hex;
|
|
use function chr;
|
|
use function count;
|
|
use function dirname;
|
|
use function explode;
|
|
use function file;
|
|
use function file_put_contents;
|
|
use function fwrite;
|
|
use function get_class;
|
|
use function implode;
|
|
use function is_array;
|
|
use function is_object;
|
|
use function json_encode;
|
|
use function ksort;
|
|
use function mkdir;
|
|
use function ord;
|
|
use function strlen;
|
|
use const FILE_IGNORE_NEW_LINES;
|
|
use const JSON_PRETTY_PRINT;
|
|
use const JSON_THROW_ON_ERROR;
|
|
use const PHP_BINARY;
|
|
use const PHP_EOL;
|
|
use const SORT_NUMERIC;
|
|
use const SORT_STRING;
|
|
use const STDERR;
|
|
|
|
require dirname(__DIR__) . '/vendor/autoload.php';
|
|
|
|
class ParserPacketHandler extends PacketHandler{
|
|
|
|
public ?ItemTypeDictionary $itemTypeDictionary = null;
|
|
private BlockTranslator $blockTranslator;
|
|
private BlockItemIdMap $blockItemIdMap;
|
|
|
|
public function __construct(private string $bedrockDataPath){
|
|
$this->blockTranslator = new BlockTranslator(
|
|
BlockStateDictionary::loadFromString(
|
|
Filesystem::fileGetContents(Path::join($this->bedrockDataPath, "canonical_block_states.nbt")),
|
|
Filesystem::fileGetContents(Path::join($this->bedrockDataPath, "block_state_meta_map.json")),
|
|
),
|
|
GlobalBlockStateHandlers::getSerializer()
|
|
);
|
|
$this->blockItemIdMap = BlockItemIdMap::getInstance();
|
|
}
|
|
|
|
private static function blockStatePropertiesToString(BlockStateData $blockStateData) : string{
|
|
$statePropertiesTag = CompoundTag::create();
|
|
foreach(Utils::stringifyKeys($blockStateData->getStates()) as $name => $value){
|
|
$statePropertiesTag->setTag($name, $value);
|
|
}
|
|
return base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($statePropertiesTag)));
|
|
}
|
|
|
|
private function itemStackToJson(ItemStack $itemStack) : ItemStackData{
|
|
if($itemStack->getId() === 0){
|
|
throw new InvalidArgumentException("Cannot serialize a null itemstack");
|
|
}
|
|
if($this->itemTypeDictionary === null){
|
|
throw new PacketHandlingException("Can't process item yet; haven't received item type dictionary");
|
|
}
|
|
$itemStringId = $this->itemTypeDictionary->fromIntId($itemStack->getId());
|
|
$data = new ItemStackData($itemStringId);
|
|
|
|
if($itemStack->getCount() !== 1){
|
|
$data->count = $itemStack->getCount();
|
|
}
|
|
|
|
$meta = $itemStack->getMeta();
|
|
if($meta === 32767){
|
|
$meta = 0; //kick wildcard magic bullshit
|
|
}
|
|
if($this->blockItemIdMap->lookupBlockId($itemStringId) !== null){
|
|
if($meta !== 0){
|
|
throw new PacketHandlingException("Unexpected non-zero blockitem meta");
|
|
}
|
|
$blockState = $this->blockTranslator->getBlockStateDictionary()->generateDataFromStateId($itemStack->getBlockRuntimeId()) ?? null;
|
|
if($blockState === null){
|
|
throw new PacketHandlingException("Unmapped blockstate ID " . $itemStack->getBlockRuntimeId());
|
|
}
|
|
|
|
$stateProperties = $blockState->getStates();
|
|
if(count($stateProperties) > 0){
|
|
$data->block_states = self::blockStatePropertiesToString($blockState);
|
|
}
|
|
}elseif($itemStack->getBlockRuntimeId() !== ItemTranslator::NO_BLOCK_RUNTIME_ID){
|
|
throw new PacketHandlingException("Non-blockitems should have a zero block runtime ID (" . $itemStack->getBlockRuntimeId() . " on " . $itemStringId . ")");
|
|
}elseif($meta !== 0){
|
|
$data->meta = $meta;
|
|
}
|
|
|
|
$rawExtraData = $itemStack->getRawExtraData();
|
|
if($rawExtraData !== ""){
|
|
$decoder = PacketSerializer::decoder($rawExtraData, 0);
|
|
$extraData = $itemStringId === ItemTypeNames::SHIELD ? ItemStackExtraDataShield::read($decoder) : ItemStackExtraData::read($decoder);
|
|
$nbt = $extraData->getNbt();
|
|
if($nbt !== null && count($nbt) > 0){
|
|
$data->nbt = base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($nbt)));
|
|
}
|
|
|
|
if(count($extraData->getCanPlaceOn()) > 0){
|
|
$data->can_place_on = $extraData->getCanPlaceOn();
|
|
}
|
|
if(count($extraData->getCanDestroy()) > 0){
|
|
$data->can_destroy = $extraData->getCanDestroy();
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* @return mixed[]
|
|
*/
|
|
private static function objectToOrderedArray(object $object) : array{
|
|
$result = (array) ($object instanceof \JsonSerializable ? $object->jsonSerialize() : $object);
|
|
ksort($result, SORT_STRING);
|
|
|
|
foreach($result as $property => $value){
|
|
if(is_object($value)){
|
|
$result[$property] = self::objectToOrderedArray($value);
|
|
}elseif(is_array($value)){
|
|
$array = [];
|
|
foreach($value as $k => $v){
|
|
if(is_object($v)){
|
|
$array[$k] = self::objectToOrderedArray($v);
|
|
}else{
|
|
$array[$k] = $v;
|
|
}
|
|
}
|
|
|
|
$result[$property] = $array;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
private static function sort(mixed $object) : mixed{
|
|
if(is_object($object)){
|
|
return self::objectToOrderedArray($object);
|
|
}
|
|
if(is_array($object)){
|
|
$result = [];
|
|
foreach($object as $k => $v){
|
|
$result[$k] = self::sort($v);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
return $object;
|
|
}
|
|
|
|
public function handleStartGame(StartGamePacket $packet) : bool{
|
|
$this->itemTypeDictionary = new ItemTypeDictionary($packet->itemTable);
|
|
|
|
echo "updating legacy item ID mapping table\n";
|
|
$table = [];
|
|
foreach($packet->itemTable as $entry){
|
|
$table[$entry->getStringId()] = [
|
|
"runtime_id" => $entry->getNumericId(),
|
|
"component_based" => $entry->isComponentBased()
|
|
];
|
|
}
|
|
ksort($table, SORT_STRING);
|
|
file_put_contents($this->bedrockDataPath . '/required_item_list.json', json_encode($table, JSON_PRETTY_PRINT) . "\n");
|
|
|
|
foreach($packet->levelSettings->experiments->getExperiments() as $name => $experiment){
|
|
echo "Experiment \"$name\" is " . ($experiment ? "" : "not ") . "active\n";
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public function handleCreativeContent(CreativeContentPacket $packet) : bool{
|
|
echo "updating creative inventory data\n";
|
|
$items = array_map(function(CreativeContentEntry $entry) : array{
|
|
return self::objectToOrderedArray($this->itemStackToJson($entry->getItem()));
|
|
}, $packet->getEntries());
|
|
file_put_contents($this->bedrockDataPath . '/creativeitems.json', json_encode($items, JSON_PRETTY_PRINT) . "\n");
|
|
return true;
|
|
}
|
|
|
|
private function recipeIngredientToJson(RecipeIngredient $itemStack) : RecipeIngredientData{
|
|
if($this->itemTypeDictionary === null){
|
|
throw new PacketHandlingException("Can't process item yet; haven't received item type dictionary");
|
|
}
|
|
|
|
$descriptor = $itemStack->getDescriptor();
|
|
if($descriptor === null){
|
|
throw new PacketHandlingException("Can't json-serialize a null recipe ingredient");
|
|
}
|
|
$data = new RecipeIngredientData();
|
|
|
|
if($descriptor instanceof IntIdMetaItemDescriptor || $descriptor instanceof StringIdMetaItemDescriptor){
|
|
if($descriptor instanceof IntIdMetaItemDescriptor){
|
|
$data->name = $this->itemTypeDictionary->fromIntId($descriptor->getId());
|
|
}else{
|
|
$data->name = $descriptor->getId();
|
|
}
|
|
$meta = $descriptor->getMeta();
|
|
if($meta !== 32767){
|
|
$blockStateId = $this->blockTranslator->getBlockStateDictionary()->lookupStateIdFromIdMeta($data->name, $meta);
|
|
if($this->blockItemIdMap->lookupBlockId($data->name) !== null && $blockStateId !== null){
|
|
$blockState = $this->blockTranslator->getBlockStateDictionary()->generateDataFromStateId($blockStateId);
|
|
if($blockState !== null && count($blockState->getStates()) > 0){
|
|
$data->block_states = self::blockStatePropertiesToString($blockState);
|
|
}
|
|
}elseif($meta !== 0){
|
|
$data->meta = $meta;
|
|
}
|
|
}else{
|
|
$data->meta = $meta;
|
|
}
|
|
}elseif($descriptor instanceof TagItemDescriptor){
|
|
$data->tag = $descriptor->getTag();
|
|
}elseif($descriptor instanceof MolangItemDescriptor){
|
|
$data->molang_expression = $descriptor->getMolangExpression();
|
|
$data->molang_version = $descriptor->getMolangVersion();
|
|
}elseif($descriptor instanceof ComplexAliasItemDescriptor){
|
|
$data->name = $descriptor->getAlias();
|
|
}else{
|
|
throw new \UnexpectedValueException("Unknown item descriptor type " . get_class($descriptor));
|
|
}
|
|
if($itemStack->getCount() !== 1){
|
|
$data->count = $itemStack->getCount();
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
private function shapedRecipeToJson(ShapedRecipe $entry) : ShapedRecipeData{
|
|
$keys = [];
|
|
|
|
$shape = [];
|
|
$char = ord("A");
|
|
|
|
$outputsByKey = [];
|
|
foreach($entry->getInput() as $x => $row){
|
|
foreach($row as $y => $ingredient){
|
|
if($ingredient->getDescriptor() === null){
|
|
$shape[$x][$y] = " ";
|
|
}else{
|
|
$jsonIngredient = $this->recipeIngredientToJson($ingredient);
|
|
$hash = json_encode($jsonIngredient, JSON_THROW_ON_ERROR);
|
|
if(isset($keys[$hash])){
|
|
$shape[$x][$y] = $keys[$hash];
|
|
}else{
|
|
$key = chr($char);
|
|
$keys[$hash] = $shape[$x][$y] = $key;
|
|
$outputsByKey[$key] = $jsonIngredient;
|
|
$char++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$unlockingIngredients = $entry->getUnlockingRequirement()->getUnlockingIngredients();
|
|
return new ShapedRecipeData(
|
|
array_map(fn(array $array) => implode('', $array), $shape),
|
|
$outputsByKey,
|
|
array_map(fn(ItemStack $output) => $this->itemStackToJson($output), $entry->getOutput()),
|
|
$entry->getBlockName(),
|
|
$entry->getPriority(),
|
|
$unlockingIngredients !== null ? array_map(fn(RecipeIngredient $input) => $this->recipeIngredientToJson($input), $unlockingIngredients) : []
|
|
);
|
|
}
|
|
|
|
private function shapelessRecipeToJson(ShapelessRecipe $recipe) : ShapelessRecipeData{
|
|
$unlockingIngredients = $recipe->getUnlockingRequirement()->getUnlockingIngredients();
|
|
return new ShapelessRecipeData(
|
|
array_map(fn(RecipeIngredient $input) => $this->recipeIngredientToJson($input), $recipe->getInputs()),
|
|
array_map(fn(ItemStack $output) => $this->itemStackToJson($output), $recipe->getOutputs()),
|
|
$recipe->getBlockName(),
|
|
$recipe->getPriority(),
|
|
$unlockingIngredients !== null ? array_map(fn(RecipeIngredient $input) => $this->recipeIngredientToJson($input), $unlockingIngredients) : []
|
|
);
|
|
}
|
|
|
|
private function furnaceRecipeToJson(FurnaceRecipe $recipe) : FurnaceRecipeData{
|
|
return new FurnaceRecipeData(
|
|
$this->recipeIngredientToJson(new RecipeIngredient(new IntIdMetaItemDescriptor($recipe->getInputId(), $recipe->getInputMeta() ?? 32767), 1)),
|
|
$this->itemStackToJson($recipe->getResult()),
|
|
$recipe->getBlockName()
|
|
);
|
|
}
|
|
|
|
private function smithingRecipeToJson(SmithingTransformRecipe $recipe) : SmithingTransformRecipeData{
|
|
return new SmithingTransformRecipeData(
|
|
$this->recipeIngredientToJson($recipe->getTemplate()),
|
|
$this->recipeIngredientToJson($recipe->getInput()),
|
|
$this->recipeIngredientToJson($recipe->getAddition()),
|
|
$this->itemStackToJson($recipe->getOutput()),
|
|
$recipe->getBlockName()
|
|
);
|
|
}
|
|
|
|
private function smithingTrimRecipeToJson(SmithingTrimRecipe $recipe) : SmithingTrimRecipeData{
|
|
return new SmithingTrimRecipeData(
|
|
$this->recipeIngredientToJson($recipe->getTemplate()),
|
|
$this->recipeIngredientToJson($recipe->getInput()),
|
|
$this->recipeIngredientToJson($recipe->getAddition()),
|
|
$recipe->getBlockName()
|
|
);
|
|
}
|
|
|
|
public function handleCraftingData(CraftingDataPacket $packet) : bool{
|
|
echo "updating crafting data\n";
|
|
|
|
$recipesPath = Path::join($this->bedrockDataPath, "recipes");
|
|
Filesystem::recursiveUnlink($recipesPath);
|
|
@mkdir($recipesPath);
|
|
|
|
$recipes = [];
|
|
foreach($packet->recipesWithTypeIds as $entry){
|
|
static $typeMap = [
|
|
CraftingDataPacket::ENTRY_SHAPELESS => "shapeless_crafting",
|
|
CraftingDataPacket::ENTRY_SHAPED => "shaped_crafting",
|
|
CraftingDataPacket::ENTRY_FURNACE => "smelting",
|
|
CraftingDataPacket::ENTRY_FURNACE_DATA => "smelting",
|
|
CraftingDataPacket::ENTRY_MULTI => "special_hardcoded",
|
|
CraftingDataPacket::ENTRY_USER_DATA_SHAPELESS => "shapeless_shulker_box",
|
|
CraftingDataPacket::ENTRY_SHAPELESS_CHEMISTRY => "shapeless_chemistry",
|
|
CraftingDataPacket::ENTRY_SHAPED_CHEMISTRY => "shaped_chemistry",
|
|
CraftingDataPacket::ENTRY_SMITHING_TRANSFORM => "smithing",
|
|
CraftingDataPacket::ENTRY_SMITHING_TRIM => "smithing_trim",
|
|
];
|
|
if(!isset($typeMap[$entry->getTypeId()])){
|
|
throw new \UnexpectedValueException("Unknown recipe type ID " . $entry->getTypeId());
|
|
}
|
|
$mappedType = $typeMap[$entry->getTypeId()];
|
|
|
|
if($entry instanceof ShapedRecipe){
|
|
//all known recipes are currently symmetric and I don't feel like attaching a `symmetric` field to
|
|
//every shaped recipe for this - split it into a separate category instead
|
|
if(!$entry->isSymmetric()){
|
|
$recipes[$mappedType . "_asymmetric"][] = $this->shapedRecipeToJson($entry);
|
|
}else{
|
|
$recipes[$mappedType][] = $this->shapedRecipeToJson($entry);
|
|
}
|
|
}elseif($entry instanceof ShapelessRecipe){
|
|
$recipes[$mappedType][] = $this->shapelessRecipeToJson($entry);
|
|
}elseif($entry instanceof MultiRecipe){
|
|
$recipes[$mappedType][] = $entry->getRecipeId()->toString();
|
|
}elseif($entry instanceof FurnaceRecipe){
|
|
$recipes[$mappedType][] = $this->furnaceRecipeToJson($entry);
|
|
}elseif($entry instanceof SmithingTransformRecipe){
|
|
$recipes[$mappedType][] = $this->smithingRecipeToJson($entry);
|
|
}elseif($entry instanceof SmithingTrimRecipe){
|
|
$recipes[$mappedType][] = $this->smithingTrimRecipeToJson($entry);
|
|
}else{
|
|
throw new AssumptionFailedError("Unknown recipe type " . get_class($entry));
|
|
}
|
|
}
|
|
|
|
foreach($packet->potionTypeRecipes as $recipe){
|
|
$recipes["potion_type"][] = new PotionTypeRecipeData(
|
|
$this->recipeIngredientToJson(new RecipeIngredient(new IntIdMetaItemDescriptor($recipe->getInputItemId(), $recipe->getInputItemMeta()), 1)),
|
|
$this->recipeIngredientToJson(new RecipeIngredient(new IntIdMetaItemDescriptor($recipe->getIngredientItemId(), $recipe->getIngredientItemMeta()), 1)),
|
|
$this->itemStackToJson(new ItemStack($recipe->getOutputItemId(), $recipe->getOutputItemMeta(), 1, 0, "")),
|
|
);
|
|
}
|
|
|
|
if($this->itemTypeDictionary === null){
|
|
throw new AssumptionFailedError("We should have already crashed if this was null");
|
|
}
|
|
foreach($packet->potionContainerRecipes as $recipe){
|
|
$recipes["potion_container_change"][] = new PotionContainerChangeRecipeData(
|
|
$this->itemTypeDictionary->fromIntId($recipe->getInputItemId()),
|
|
$this->recipeIngredientToJson(new RecipeIngredient(new IntIdMetaItemDescriptor($recipe->getIngredientItemId(), 0), 1)),
|
|
$this->itemTypeDictionary->fromIntId($recipe->getOutputItemId()),
|
|
);
|
|
}
|
|
|
|
//this sorts the data into a canonical order to make diffs between versions reliable
|
|
//how the data is ordered doesn't matter as long as it's reproducible
|
|
foreach($recipes as $_type => $entries){
|
|
$_sortedRecipes = [];
|
|
$_seen = [];
|
|
foreach($entries as $entry){
|
|
$entry = self::sort($entry);
|
|
$_key = json_encode($entry);
|
|
$duplicates = $_seen[$_key] ??= 0;
|
|
$_seen[$_key]++;
|
|
$suffix = chr(ord("a") + $duplicates);
|
|
$_sortedRecipes[$_key . $suffix] = $entry;
|
|
}
|
|
ksort($_sortedRecipes, SORT_STRING);
|
|
$recipes[$_type] = array_values($_sortedRecipes);
|
|
foreach($_seen as $_key => $_seenCount){
|
|
if($_seenCount > 1){
|
|
fwrite(STDERR, "warning: $_type recipe $_key was seen $_seenCount times\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
ksort($recipes, SORT_STRING);
|
|
foreach($recipes as $type => $entries){
|
|
echo "$type: " . count($entries) . "\n";
|
|
}
|
|
foreach($recipes as $type => $entries){
|
|
file_put_contents(Path::join($recipesPath, $type . '.json'), json_encode($entries, JSON_PRETTY_PRINT) . "\n");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function handleAvailableActorIdentifiers(AvailableActorIdentifiersPacket $packet) : bool{
|
|
echo "storing actor identifiers" . PHP_EOL;
|
|
|
|
$tag = $packet->identifiers->getRoot();
|
|
if(!($tag instanceof CompoundTag)){
|
|
throw new AssumptionFailedError();
|
|
}
|
|
$idList = $tag->getTag("idlist");
|
|
if(!($idList instanceof ListTag) || $idList->getTagType() !== NBT::TAG_Compound){
|
|
echo $tag . "\n";
|
|
throw new \RuntimeException("expected TAG_List<TAG_Compound>(\"idlist\") tag inside root TAG_Compound");
|
|
}
|
|
if($tag->count() > 1){
|
|
echo $tag . "\n";
|
|
echo "!!! unexpected extra data found in available actor identifiers\n";
|
|
}
|
|
echo "updating legacy => string entity ID mapping table\n";
|
|
$map = [];
|
|
/**
|
|
* @var CompoundTag $thing
|
|
*/
|
|
foreach($idList as $thing){
|
|
$map[$thing->getString("id")] = $thing->getInt("rid");
|
|
}
|
|
asort($map, SORT_NUMERIC);
|
|
file_put_contents($this->bedrockDataPath . '/entity_id_map.json', json_encode($map, JSON_PRETTY_PRINT) . "\n");
|
|
echo "storing entity identifiers\n";
|
|
file_put_contents($this->bedrockDataPath . '/entity_identifiers.nbt', $packet->identifiers->getEncodedNbt());
|
|
return true;
|
|
}
|
|
|
|
public function handleBiomeDefinitionList(BiomeDefinitionListPacket $packet) : bool{
|
|
echo "storing biome definitions" . PHP_EOL;
|
|
|
|
file_put_contents($this->bedrockDataPath . '/biome_definitions_full.nbt', $packet->definitions->getEncodedNbt());
|
|
|
|
$nbt = $packet->definitions->getRoot();
|
|
if(!$nbt instanceof CompoundTag){
|
|
throw new AssumptionFailedError();
|
|
}
|
|
$strippedNbt = clone $nbt;
|
|
foreach($strippedNbt as $compound){
|
|
if($compound instanceof CompoundTag){
|
|
foreach([
|
|
"minecraft:capped_surface",
|
|
"minecraft:consolidated_features",
|
|
"minecraft:frozen_ocean_surface",
|
|
"minecraft:legacy_world_generation_rules",
|
|
"minecraft:mesa_surface",
|
|
"minecraft:mountain_parameters",
|
|
"minecraft:multinoise_generation_rules",
|
|
"minecraft:overworld_generation_rules",
|
|
"minecraft:surface_material_adjustments",
|
|
"minecraft:surface_parameters",
|
|
"minecraft:swamp_surface",
|
|
] as $remove){
|
|
$compound->removeTag($remove);
|
|
}
|
|
}
|
|
}
|
|
|
|
file_put_contents($this->bedrockDataPath . '/biome_definitions.nbt', (new CacheableNbt($strippedNbt))->getEncodedNbt());
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param string[] $argv
|
|
*/
|
|
function main(array $argv) : int{
|
|
if(count($argv) !== 3){
|
|
fwrite(STDERR, 'Usage: ' . PHP_BINARY . ' ' . __FILE__ . ' <input file> <path to BedrockData>');
|
|
return 1;
|
|
}
|
|
[, $inputFile, $bedrockDataPath] = $argv;
|
|
|
|
$handler = new ParserPacketHandler($bedrockDataPath);
|
|
|
|
$packets = file($inputFile, FILE_IGNORE_NEW_LINES);
|
|
if($packets === false){
|
|
fwrite(STDERR, 'File ' . $inputFile . ' not found or permission denied');
|
|
return 1;
|
|
}
|
|
|
|
foreach($packets as $lineNum => $line){
|
|
$parts = explode(':', $line);
|
|
if(count($parts) !== 2){
|
|
fwrite(STDERR, 'Wrong packet format at line ' . ($lineNum + 1) . ', expected read:base64 or write:base64');
|
|
return 1;
|
|
}
|
|
$raw = base64_decode($parts[1], true);
|
|
if($raw === false){
|
|
fwrite(STDERR, 'Invalid base64\'d packet on line ' . ($lineNum + 1) . ' could not be parsed');
|
|
return 1;
|
|
}
|
|
|
|
$pk = PacketPool::getInstance()->getPacket($raw);
|
|
if($pk === null){
|
|
fwrite(STDERR, "Unknown packet on line " . ($lineNum + 1) . ": " . $parts[1]);
|
|
continue;
|
|
}
|
|
$serializer = PacketSerializer::decoder($raw, 0);
|
|
|
|
$pk->decode($serializer);
|
|
$pk->handle($handler);
|
|
if(!$serializer->feof()){
|
|
echo "Packet on line " . ($lineNum + 1) . ": didn't read all data from " . get_class($pk) . " (stopped at offset " . $serializer->getOffset() . " of " . strlen($serializer->getBuffer()) . " bytes): " . bin2hex($serializer->getRemaining()) . "\n";
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
exit(main($argv));
|