forked from EXTERNAL/bareiron
452 lines
13 KiB
JavaScript
452 lines
13 KiB
JavaScript
const fs = require("fs/promises");
|
|
const path = require("path");
|
|
|
|
// Overrides for block-to-item conversion
|
|
const blockToItemOverrides = {
|
|
"grass_block": "dirt",
|
|
"snowy_grass_block": "dirt",
|
|
"stone": "cobblestone",
|
|
"diamond_ore": "diamond",
|
|
"gold_ore": "raw_gold",
|
|
"redstone_ore": "redstone",
|
|
"iron_ore": "raw_iron",
|
|
"coal_ore": "coal",
|
|
"snow": "snowball",
|
|
"dead_bush": "stick"
|
|
};
|
|
|
|
// Blacklisted block name strings
|
|
const blockBlacklist = [
|
|
"spruce_",
|
|
"birch_",
|
|
"jungle_",
|
|
"acacia_",
|
|
"dark_oak_",
|
|
"mangrove_",
|
|
"cherry_",
|
|
"pale_oak_",
|
|
"crimson_",
|
|
"warped_",
|
|
"bamboo_",
|
|
"deepslate",
|
|
"infested_",
|
|
"stained_",
|
|
"_terracotta",
|
|
"_head"
|
|
];
|
|
|
|
// Whitelisted blocks, i.e. guaranteed to be included
|
|
const blockWhitelist = [
|
|
"air",
|
|
"water",
|
|
"lava",
|
|
"snowy_grass_block",
|
|
"mud",
|
|
"moss_carpet",
|
|
"oak_slab",
|
|
"stone_slab",
|
|
"cobblestone_slab",
|
|
"composter",
|
|
"coal_block"
|
|
];
|
|
|
|
// Currently, only 4 biome types are supported, excluding "beach"
|
|
const biomes = [
|
|
"plains",
|
|
"mangrove_swamp",
|
|
"desert",
|
|
"snowy_plains",
|
|
"beach"
|
|
];
|
|
|
|
// Extract item and block data from registry dump
|
|
async function extractItemsAndBlocks () {
|
|
|
|
// Block network IDs are defined in their own JSON file
|
|
// The item JSON file doesn't define IDs, we get those from the registries
|
|
const blockSource = JSON.parse(await fs.readFile(`${__dirname}/notchian/generated/reports/blocks.json`, "utf8"));
|
|
|
|
// Get registry data for extracting item IDs
|
|
const registriesJSON = JSON.parse(await fs.readFile(`${__dirname}/notchian/generated/reports/registries.json`, "utf8"));
|
|
const itemSource = registriesJSON["minecraft:item"].entries;
|
|
// Retrieve the registry list for blocks too, used later in tags
|
|
const blockRegistrySource = registriesJSON["minecraft:block"].entries;
|
|
|
|
// Sort blocks by their network ID
|
|
// Since we're only storing 256 blocks, this prioritizes the "common" ones first
|
|
const sortedBlocks = Object.entries(blockSource);
|
|
sortedBlocks.sort((a, b) => {
|
|
const aState = a[1].states.find(c => c.default);
|
|
if (!aState) return 1;
|
|
const bState = b[1].states.find(c => c.default);
|
|
if (!bState) return -1;
|
|
return aState.id - bState.id;
|
|
});
|
|
|
|
// Create name-id pair objects for easier parsing
|
|
const blocks = {}, items = {};
|
|
|
|
for (const entry of sortedBlocks) {
|
|
const defaultState = entry[1].states.find(c => c.default);
|
|
if (!defaultState) continue;
|
|
// Check if a part of this block's name is in the blacklist
|
|
let found = false;
|
|
for (const str of blockBlacklist) {
|
|
if (entry[0].includes(str)) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (found) continue;
|
|
// Register the block ID
|
|
blocks[entry[0].replace("minecraft:", "")] = defaultState.id;
|
|
// Include "snowy" variants of blocks as well
|
|
if ("properties" in defaultState && "snowy" in defaultState.properties) {
|
|
const snowyState = entry[1].states.find(c => c.properties.snowy);
|
|
blocks["snowy_" + entry[0].replace("minecraft:", "")] = snowyState.id;
|
|
}
|
|
}
|
|
|
|
for (const item in itemSource) {
|
|
items[item.replace("minecraft:", "")] = itemSource[item].protocol_id;
|
|
}
|
|
|
|
/**
|
|
* Create a block network ID palette.
|
|
* The goal is to pack as many meaningful blocks into 256 IDs as
|
|
* possible. For this, we only include blocks that have corresponding
|
|
* items, outside of some exceptions.
|
|
*/
|
|
const palette = {};
|
|
|
|
// While we're at it, map block IDs to item IDs
|
|
const mapping = [], mappingWithOverrides = [];
|
|
|
|
// Handle explicitly whitelisted blocks first
|
|
for (const block of blockWhitelist) {
|
|
palette[block] = blocks[block];
|
|
mapping.push(items[block] || 0);
|
|
mappingWithOverrides.push(items[blockToItemOverrides[block]] || items[block] || 0);
|
|
if (mapping.length === 256) break;
|
|
}
|
|
|
|
// Continue adding blocks with matching items
|
|
for (const block in blocks) {
|
|
if (!(block in items)) continue;
|
|
if (blockWhitelist.includes(block)) continue;
|
|
palette[block] = blocks[block];
|
|
mapping.push(items[block] || 0);
|
|
mappingWithOverrides.push(items[blockToItemOverrides[block]] || items[block] || 0);
|
|
if (mapping.length === 256) break;
|
|
}
|
|
|
|
// Build list of block IDs, but from the registries
|
|
// Tags refer to these IDs, not the actual blocks
|
|
const blockRegistry = {};
|
|
for (const block in blockRegistrySource) {
|
|
blockRegistry[block.replace("minecraft:", "")] = blockRegistrySource[block].protocol_id;
|
|
}
|
|
|
|
return { blocks, items, palette, mapping, mappingWithOverrides, blockRegistry };
|
|
|
|
}
|
|
|
|
// Write an integer as a VarInt
|
|
function writeVarInt (value) {
|
|
const bytes = [];
|
|
while (true) {
|
|
if ((value & ~0x7F) === 0) {
|
|
bytes.push(value);
|
|
return Buffer.from(bytes);
|
|
}
|
|
bytes.push((value & 0x7F) | 0x80);
|
|
value >>>= 7;
|
|
}
|
|
}
|
|
|
|
// Scan directory recursively to find all JSON files
|
|
async function scanDirectory (basePath, currentPath = "") {
|
|
const entries = {};
|
|
const items = await fs.readdir(path.join(basePath, currentPath), { withFileTypes: true });
|
|
|
|
for (const item of items) {
|
|
const relativePath = path.join(currentPath, item.name);
|
|
if (item.isDirectory()) {
|
|
const subEntries = await scanDirectory(basePath, relativePath);
|
|
Object.assign(entries, subEntries);
|
|
} else if (item.name.endsWith(".json")) {
|
|
const dirPath = path.dirname(relativePath);
|
|
const registryName = dirPath === "." ? "" : dirPath;
|
|
const entryName = path.basename(item.name, ".json");
|
|
|
|
if (!entries[registryName]) {
|
|
entries[registryName] = [];
|
|
}
|
|
entries[registryName].push(entryName);
|
|
}
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
// Serialize a single registry
|
|
function serializeRegistry (name, entries) {
|
|
const parts = [];
|
|
|
|
// Packet ID for Registry Data
|
|
parts.push(Buffer.from([0x07]));
|
|
|
|
// Registry name
|
|
const nameBuf = Buffer.from(name, "utf8");
|
|
parts.push(writeVarInt(nameBuf.length));
|
|
parts.push(nameBuf);
|
|
|
|
// Entry count
|
|
parts.push(writeVarInt(entries.length));
|
|
|
|
// Serialize entries
|
|
for (const entryName of entries) {
|
|
const entryBuf = Buffer.from(entryName, "utf8");
|
|
parts.push(writeVarInt(entryBuf.length));
|
|
parts.push(entryBuf);
|
|
parts.push(Buffer.from([0x00]));
|
|
}
|
|
|
|
// Combine all parts
|
|
const fullData = Buffer.concat(parts);
|
|
|
|
// Prepend packet length
|
|
const lengthBuf = writeVarInt(fullData.length);
|
|
|
|
return Buffer.concat([lengthBuf, fullData]);
|
|
}
|
|
|
|
// Serialize a tag update
|
|
function serializeTags (tags) {
|
|
const parts = [];
|
|
|
|
// Packet ID for Update Tags
|
|
parts.push(Buffer.from([0x0D]));
|
|
|
|
// Tag type count
|
|
parts.push(writeVarInt(Object.keys(tags).length));
|
|
|
|
// Tag registry entry
|
|
for (const type in tags) {
|
|
|
|
// Tag registry identifier
|
|
const identifier = Buffer.from(type, "utf8");
|
|
parts.push(writeVarInt(identifier.length));
|
|
parts.push(identifier);
|
|
|
|
// Tag count
|
|
parts.push(writeVarInt(Object.keys(tags[type]).length));
|
|
|
|
// Write tag data
|
|
for (const tag in tags[type]) {
|
|
// Tag identifier
|
|
const identifier = Buffer.from(tag, "utf8");
|
|
parts.push(writeVarInt(identifier.length));
|
|
parts.push(identifier);
|
|
// Array of IDs
|
|
parts.push(writeVarInt(Object.keys(tags[type][tag]).length));
|
|
for (const id of tags[type][tag]) {
|
|
parts.push(writeVarInt(id));
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Combine all parts
|
|
const fullData = Buffer.concat(parts);
|
|
|
|
// Prepend packet length
|
|
const lengthBuf = writeVarInt(fullData.length);
|
|
|
|
return Buffer.concat([lengthBuf, fullData]);
|
|
}
|
|
|
|
function toVarIntBuffer (array) {
|
|
const parts = [];
|
|
for (const num of array) {
|
|
parts.push(writeVarInt(num));
|
|
}
|
|
return Buffer.concat(parts);
|
|
}
|
|
|
|
// Convert to C-style hex byte array string
|
|
function toCArray (buffer) {
|
|
const hexBytes = [...buffer].map(b => `0x${b.toString(16).padStart(2, "0")}`);
|
|
const lines = [];
|
|
for (let i = 0; i < hexBytes.length; i += 12) {
|
|
lines.push(" " + hexBytes.slice(i, i + 12).join(", "));
|
|
}
|
|
return lines.join(",\n");
|
|
}
|
|
|
|
const requiredRegistries = [
|
|
"dimension_type",
|
|
"cat_variant",
|
|
"chicken_variant",
|
|
"cow_variant",
|
|
"frog_variant",
|
|
"painting_variant",
|
|
"pig_variant",
|
|
"wolf_sound_variant",
|
|
"wolf_variant",
|
|
"damage_type"
|
|
];
|
|
|
|
async function convert () {
|
|
|
|
const inputPath = __dirname + "/notchian/generated/data/minecraft";
|
|
const outputPath = __dirname + "/src/registries.c";
|
|
const headerPath = __dirname + "/include/registries.h";
|
|
|
|
const registries = await scanDirectory(inputPath);
|
|
const registryBuffers = [];
|
|
|
|
for (const registry of requiredRegistries) {
|
|
if (!(registry in registries)) {
|
|
console.error(`Missing required registry "${registry}"!`);
|
|
return;
|
|
}
|
|
if (registry.endsWith("variant")) {
|
|
// The mob "variants" only require one valid variant to be accepted
|
|
// Send "temperate" if available, otherwise shortest string to save memory
|
|
if (registries[registry].includes("temperate")) {
|
|
registryBuffers.push(serializeRegistry(registry, ["temperate"]));
|
|
} else {
|
|
const shortest = registries[registry].sort((a, b) => a.length - b.length)[0];
|
|
registryBuffers.push(serializeRegistry(registry, [shortest]));
|
|
}
|
|
} else {
|
|
registryBuffers.push(serializeRegistry(registry, registries[registry]));
|
|
}
|
|
}
|
|
// Send biomes separately - only "plains" is actually required
|
|
registryBuffers.push(serializeRegistry("worldgen/biome", biomes));
|
|
const fullRegistryBuffer = Buffer.concat(registryBuffers);
|
|
|
|
const itemsAndBlocks = await extractItemsAndBlocks();
|
|
|
|
const tagBuffer = serializeTags({
|
|
"fluid": {
|
|
"water": [ 2 ] // source water block
|
|
},
|
|
"block": {
|
|
"mineable/pickaxe": [
|
|
itemsAndBlocks.blockRegistry["stone"],
|
|
itemsAndBlocks.blockRegistry["stone_slab"],
|
|
itemsAndBlocks.blockRegistry["cobblestone"],
|
|
itemsAndBlocks.blockRegistry["cobblestone_slab"],
|
|
itemsAndBlocks.blockRegistry["sandstone"],
|
|
itemsAndBlocks.blockRegistry["sandstone_slab"],
|
|
itemsAndBlocks.blockRegistry["ice"],
|
|
itemsAndBlocks.blockRegistry["diamond_ore"],
|
|
itemsAndBlocks.blockRegistry["gold_ore"],
|
|
itemsAndBlocks.blockRegistry["redstone_ore"],
|
|
itemsAndBlocks.blockRegistry["iron_ore"],
|
|
itemsAndBlocks.blockRegistry["coal_ore"],
|
|
itemsAndBlocks.blockRegistry["furnace"],
|
|
itemsAndBlocks.blockRegistry["iron_block"],
|
|
itemsAndBlocks.blockRegistry["gold_block"],
|
|
itemsAndBlocks.blockRegistry["diamond_block"],
|
|
itemsAndBlocks.blockRegistry["redstone_block"],
|
|
itemsAndBlocks.blockRegistry["coal_block"]
|
|
],
|
|
"mineable/axe": [
|
|
itemsAndBlocks.blockRegistry["oak_log"],
|
|
itemsAndBlocks.blockRegistry["oak_planks"],
|
|
itemsAndBlocks.blockRegistry["oak_wood"],
|
|
itemsAndBlocks.blockRegistry["oak_slab"],
|
|
itemsAndBlocks.blockRegistry["crafting_table"]
|
|
],
|
|
"mineable/shovel": [
|
|
itemsAndBlocks.blockRegistry["grass_block"],
|
|
itemsAndBlocks.blockRegistry["dirt"],
|
|
itemsAndBlocks.blockRegistry["sand"],
|
|
itemsAndBlocks.blockRegistry["snow"],
|
|
itemsAndBlocks.blockRegistry["snow_block"],
|
|
itemsAndBlocks.blockRegistry["mud"]
|
|
],
|
|
},
|
|
"item": {
|
|
"planks": [
|
|
itemsAndBlocks.items["oak_planks"]
|
|
]
|
|
}
|
|
});
|
|
|
|
const networkBlockPalette = toVarIntBuffer(Object.values(itemsAndBlocks.palette));
|
|
|
|
const sourceCode = `\
|
|
#include <stdint.h>
|
|
#include "registries.h"
|
|
|
|
// Binary contents of required "Registry Data" packets
|
|
uint8_t registries_bin[] = {
|
|
${toCArray(fullRegistryBuffer)}
|
|
};
|
|
// Binary contents of "Update Tags" packets
|
|
uint8_t tags_bin[] = {
|
|
${toCArray(tagBuffer)}
|
|
};
|
|
|
|
// Block palette
|
|
uint16_t block_palette[] = { ${Object.values(itemsAndBlocks.palette).join(", ")} };
|
|
// Block palette as VarInt buffer
|
|
uint8_t network_block_palette[] = {
|
|
${toCArray(networkBlockPalette)}
|
|
};
|
|
|
|
// Block-to-item mapping
|
|
uint16_t B_to_I[] = { ${itemsAndBlocks.mappingWithOverrides.join(", ")} };
|
|
// Item-to-block mapping
|
|
uint8_t I_to_B (uint16_t item) {
|
|
switch (item) {
|
|
${itemsAndBlocks.mapping.map((c, i) => c ? `case ${c}: return ${i};\n ` : "").join("")}
|
|
default: break;
|
|
}
|
|
return 0;
|
|
}
|
|
`;
|
|
|
|
const headerCode = `\
|
|
#ifndef H_REGISTRIES
|
|
#define H_REGISTRIES
|
|
|
|
#include <stdint.h>
|
|
|
|
// Binary packet data (${fullRegistryBuffer.length + tagBuffer.length} bytes total)
|
|
extern uint8_t registries_bin[${fullRegistryBuffer.length}];
|
|
extern uint8_t tags_bin[${tagBuffer.length}];
|
|
|
|
extern uint16_t block_palette[256]; // Block palette
|
|
extern uint8_t network_block_palette[${networkBlockPalette.length}]; // Block palette as VarInt buffer
|
|
extern uint16_t B_to_I[256]; // Block-to-item mapping
|
|
uint8_t I_to_B (uint16_t item); // Item-to-block mapping
|
|
|
|
// Block identifiers
|
|
${Object.keys(itemsAndBlocks.palette).map((c, i) => `#define B_${c} ${i}`).join("\n")}
|
|
|
|
// Item identifiers
|
|
${Object.entries(itemsAndBlocks.items).map(c => `#define I_${c[0]} ${c[1]}`).join("\n")}
|
|
|
|
// Biome identifiers
|
|
${biomes.map((c, i) => `#define W_${c} ${i}`).join("\n")}
|
|
|
|
// Damage type identifiers
|
|
${registries["damage_type"].map((c, i) => `#define D_${c} ${i}`).join("\n")}
|
|
|
|
#endif
|
|
`;
|
|
|
|
await fs.writeFile(outputPath, sourceCode);
|
|
await fs.writeFile(headerPath, headerCode);
|
|
console.log("Done. Wrote to `registries.c` and `registries.h`");
|
|
|
|
}
|
|
|
|
convert().catch(console.error);
|