mirror of
https://github.com/pmmp/PocketMine-MP.git
synced 2025-06-07 04:17:07 +00:00
this was achieved by storing binary representations of the blockstates, rather than the original BlockStateData. Due to the insane object:data ratio of Tag objects (40:1 for ByteTag for example), modestly sized NBT can explode in memory footprint. This has been previously seen with the absurd 25 MB footprint on file load. Previously, I attempted to mitigate this by deduplicating tag objects, but this was mitigating a symptom rather than addressing the cause. We don't actually need to keep the NBT around in memory, since we don't actually use it for anything other than matching blockstates. In this case, we can allow the code to be possibly a little slower, since the lookup is anyway slow and the result will be cached. In fact, using encoded ordered states as hash keys significantly improves the speed of lookups for stuff like walls, which have many thousands of states. We keep around generateStateData(), since it's still possible we may need the BlockStateData associated, and it can be easily reconstructed from the binary-encoded representation in BlockStateDictionaryEntry.
577 lines
21 KiB
PHP
577 lines
21 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\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\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\serializer\PacketSerializerContext;
|
|
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\ItemTypeEntry;
|
|
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;
|
|
|
|
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()
|
|
);
|
|
}
|
|
|
|
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");
|
|
}
|
|
$data = new ItemStackData($this->itemTypeDictionary->fromIntId($itemStack->getId()));
|
|
|
|
if($itemStack->getCount() !== 1){
|
|
$data->count = $itemStack->getCount();
|
|
}
|
|
|
|
$meta = $itemStack->getMeta();
|
|
if($meta === 32767){
|
|
$meta = 0; //kick wildcard magic bullshit
|
|
}
|
|
if($itemStack->getBlockRuntimeId() !== 0){
|
|
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($meta !== 0){
|
|
$data->meta = $meta;
|
|
}
|
|
|
|
$nbt = $itemStack->getNbt();
|
|
if($nbt !== null && count($nbt) > 0){
|
|
$data->nbt = base64_encode((new LittleEndianNbtSerializer())->write(new TreeRoot($nbt)));
|
|
}
|
|
|
|
if(count($itemStack->getCanPlaceOn()) > 0){
|
|
$data->can_place_on = $itemStack->getCanPlaceOn();
|
|
}
|
|
if(count($itemStack->getCanDestroy()) > 0){
|
|
$data->can_destroy = $itemStack->getCanDestroy();
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* @return mixed[]
|
|
*/
|
|
private static function objectToOrderedArray(object $object) : array{
|
|
$result = (array) $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($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++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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()
|
|
);
|
|
}
|
|
|
|
private function shapelessRecipeToJson(ShapelessRecipe $recipe) : ShapelessRecipeData{
|
|
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()
|
|
);
|
|
}
|
|
|
|
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_SHULKER_BOX => "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){
|
|
$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, null, [], [], null)),
|
|
);
|
|
}
|
|
|
|
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 = [];
|
|
foreach($entries as $entry){
|
|
$entry = self::sort($entry);
|
|
$_key = json_encode($entry);
|
|
while(isset($_sortedRecipes[$_key])){
|
|
echo "warning: duplicated $_type recipe: $_key\n";
|
|
$_key .= "a";
|
|
}
|
|
$_sortedRecipes[$_key] = $entry;
|
|
}
|
|
ksort($_sortedRecipes, SORT_STRING);
|
|
$recipes[$_type] = array_values($_sortedRecipes);
|
|
}
|
|
|
|
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, new PacketSerializerContext(
|
|
$handler->itemTypeDictionary ??
|
|
new ItemTypeDictionary([new ItemTypeEntry("minecraft:shield", 0, false)]))
|
|
);
|
|
|
|
$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));
|