minecraft data raw code
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
/node_modules
|
||||
1.*
|
||||
bun.lock
|
||||
pnpm-lock.yaml
|
||||
package-lock.json
|
||||
__*
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* This is a utility script that converts the YAML here into ProtoDef schema code
|
||||
* You can run this with `npm run build`
|
||||
*/
|
||||
const fs = require('fs')
|
||||
const { join } = require('path')
|
||||
const { parse, compile } = require('protodef-yaml')
|
||||
|
||||
function getJSON (path) {
|
||||
return JSON.parse(fs.readFileSync(path, 'utf-8'))
|
||||
}
|
||||
|
||||
// Parse the YML files and turn to JSON
|
||||
function genProtoSchema (createPacketMap) {
|
||||
// Create the packet_map.yml from proto.yml
|
||||
const parsed = parse('./proto.yml')
|
||||
const version = parsed['!version']
|
||||
|
||||
if (createPacketMap) {
|
||||
// On bedrock the packet maps are created automatically here
|
||||
const packets = []
|
||||
for (const key in parsed) {
|
||||
if (key.startsWith('%container')) {
|
||||
const [, name] = key.split(',')
|
||||
if (name.startsWith('packet_')) {
|
||||
const children = parsed[key]
|
||||
const packetName = name.replace('packet_', '')
|
||||
const packetID = children['!id']
|
||||
packets.push([packetID, packetName, name])
|
||||
}
|
||||
}
|
||||
}
|
||||
let l1 = ''
|
||||
let l2 = ''
|
||||
for (const [id, name, fname] of packets) {
|
||||
l1 += ` 0x${id.toString(16).padStart(2, '0')}: ${name}\n`
|
||||
l2 += ` if ${name}: ${fname}\n`
|
||||
}
|
||||
// TODO: skip creating packet_map.yml and just generate the ProtoDef map JSON directly
|
||||
const t = `#Auto-generated from proto.yml, do not modify\n!import: types.yml\nmcpe_packet:\n name: varint =>\n${l1}\n params: name ?\n${l2}`
|
||||
fs.writeFileSync('./packet_map.yml', t)
|
||||
}
|
||||
|
||||
const json = compile('./proto.yml')
|
||||
fs.rmSync('./packet_map.yml', { force: true }) // temp file
|
||||
return [version, json]
|
||||
}
|
||||
|
||||
function visitor (key, value) {
|
||||
// Convert decimal to hex in protocol.json for packet mapper fields
|
||||
if ((key === 'packet') && (value?.[1]?.[0]?.name === 'name')) {
|
||||
const mapper = value[1][0].type[1].mappings
|
||||
for (const key in mapper) {
|
||||
if (key.startsWith('0x')) continue
|
||||
mapper['0x' + Number(key).toString(16).padStart(2, '0')] = mapper[key]
|
||||
delete mapper[key]
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function convert (edition, ver, path) {
|
||||
process.chdir(path || join(__dirname, `../../data/${edition}/${ver}`))
|
||||
const [version, json] = genProtoSchema(edition === 'bedrock')
|
||||
fs.mkdirSync(`../${version}`, { recursive: true })
|
||||
if (edition === 'bedrock') {
|
||||
fs.writeFileSync(`../${version}/protocol.json`, JSON.stringify({ types: json }, visitor, 2))
|
||||
} else if (edition === 'pc') {
|
||||
fs.writeFileSync(`../${version}/protocol.json`, JSON.stringify(json, visitor, 2))
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
function validate (edition, ver, path) {
|
||||
process.chdir(path || join(__dirname, `../../data/${edition}/${ver}`))
|
||||
// console.log(process.cwd())
|
||||
const [version, json] = genProtoSchema(edition === 'bedrock')
|
||||
|
||||
if (path.endsWith('latest')) {
|
||||
if (fs.existsSync(join(__dirname, `../../data/${edition}/${version}/proto.yml`))) {
|
||||
throw Error(`Both ${edition}/latest/proto.yml and ${edition}/${version}/proto.yml exist for ${version}, please remove the latter.`)
|
||||
}
|
||||
}
|
||||
|
||||
const expected = edition === 'bedrock'
|
||||
? JSON.stringify({ types: json }, visitor, 2)
|
||||
: JSON.stringify(json, visitor, 2)
|
||||
|
||||
// If you crash here, no protocol.json was generated - run `npm run build`
|
||||
const actual = JSON.stringify(getJSON(`../${version}/protocol.json`), null, 2)
|
||||
|
||||
// Make sure the protocol_expected.json file equals the protocol.json file; otherwise the JSON must be rebuilt
|
||||
if (expected !== actual) {
|
||||
// console.log(expected.length, actual.length, expected, actual)
|
||||
throw Error(`${ver} (${version}) / protocol.json is desynced from yaml, please run 'npm run build'`)
|
||||
}
|
||||
|
||||
// console.log('ok', `../${version}/protocol.json`)
|
||||
return version
|
||||
}
|
||||
|
||||
const dataPaths = require('../../data/dataPaths.json')
|
||||
function all (fn) {
|
||||
for (const edition in dataPaths) {
|
||||
const versions = dataPaths[edition]
|
||||
for (const versionId in versions) {
|
||||
console.log('⏳', fn.name, 'protocol for', edition, versionId)
|
||||
const paths = versions[versionId]
|
||||
if (paths.proto) {
|
||||
fn(edition, versionId, join(__dirname, '../../data/' + paths.proto))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no argument, build everything
|
||||
if (!module.parent) {
|
||||
if (!process.argv[2]) {
|
||||
all(convert)
|
||||
} else { // build the specified version
|
||||
convert(process.argv[2], process.argv[3])
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { convert, validate, all }
|
||||
@@ -0,0 +1,65 @@
|
||||
const { genYAML } = require('protodef-yaml')
|
||||
const { join } = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
function addDataPath (version, key, value) {
|
||||
const dataPaths = require('../../data/dataPaths.json')
|
||||
if (dataPaths.pc[version][key]) return
|
||||
dataPaths.pc[version][key] = value
|
||||
console.log('Updating datapath for', version, key, value)
|
||||
fs.writeFileSync(join(__dirname, '../../data/dataPaths.json'), JSON.stringify(dataPaths, null, 2))
|
||||
}
|
||||
|
||||
function convert (version) {
|
||||
let text = '!version: ' + version + '\n\n'
|
||||
const protocol = require(`../../data/pc/${version}/protocol.json`)
|
||||
for (const state in protocol) {
|
||||
const data = protocol[state]
|
||||
if (state === 'types') {
|
||||
text += '^types:\n'
|
||||
text += genYAML(data, 1)
|
||||
}
|
||||
// 0.30c (stateless)
|
||||
if (state === 'toClient') {
|
||||
text += '\n^toClient.types:\n'
|
||||
text += genYAML(data.types, 1)
|
||||
}
|
||||
if (state === 'toServer') {
|
||||
text += '\n^toServer.types:\n'
|
||||
text += genYAML(data.types, 1)
|
||||
}
|
||||
// Rest of versions have states
|
||||
if (data.toClient) {
|
||||
text += `\n^${state}.toClient.types:\n`
|
||||
text += genYAML(data.toClient.types, 1)
|
||||
}
|
||||
if (data.toServer) {
|
||||
text += `\n^${state}.toServer.types:\n`
|
||||
text += genYAML(data.toServer.types, 1)
|
||||
}
|
||||
}
|
||||
const outFile = `../../data/pc/${version}/proto.yml`
|
||||
const absPath = fs.realpathSync(`../../data/pc/${version}/`) + '/proto.yml'
|
||||
console.log(`Wrote to ${absPath}`)
|
||||
fs.writeFileSync(outFile, text)
|
||||
addDataPath(version, 'proto', 'pc/' + version)
|
||||
}
|
||||
|
||||
if (process.argv.length < 3) {
|
||||
console.log('Usage: node convertMcpcProtocolJsonToYaml.js <version>')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const version = process.argv[2]
|
||||
if (version === 'all') {
|
||||
const dataPaths = require('../../data/dataPaths.json')
|
||||
for (const version in dataPaths.pc) {
|
||||
try {
|
||||
convert(version)
|
||||
} catch (e) {
|
||||
console.log('Failed for', version, e)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
convert(version)
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs')
|
||||
const cp = require('child_process')
|
||||
const { globSync } = require('glob')
|
||||
|
||||
/**
|
||||
* Normalize decompiler output into an array of "statements"/lines.
|
||||
* @param {string} raw
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function prepLines (raw) {
|
||||
const lines = raw.replaceAll(' {\n', ' {;\n').split(';')
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim()
|
||||
if (line.includes('static final') && !line.includes(' = ')) {
|
||||
const varName = line.split(' ').pop()
|
||||
for (let j = 0; j < lines.length; j++) {
|
||||
const line2 = lines[j].trim()
|
||||
if (line2.startsWith(varName + ' = ')) {
|
||||
lines[i] = line.replace(varName, line2)
|
||||
lines[j] = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
/**
|
||||
* Read entity types from the client EntityType.java and return mappings.
|
||||
* @param {string} versionDir
|
||||
* @returns {[Record<string,string>, Record<string,string>]}
|
||||
*/
|
||||
function getEntityTypes (versionDir) {
|
||||
const entityTypes = fs.readFileSync(`${versionDir}/client/net/minecraft/world/entity/EntityType.java`, 'utf8')
|
||||
const entityTypesLines = prepLines(entityTypes)
|
||||
const classNameTo = {}
|
||||
const nameToClass = {}
|
||||
for (const line of entityTypesLines) {
|
||||
if (line.includes('= register(')) {
|
||||
// Given the line: public static final EntityType<Allay> ALLAY = register( "allay", EntityType.Builder.<Allay>of(Allay::new, MobCategory.CREATURE).sized(0.35F, 0.6F).clientTrackingRange(8).updateInterval(2) );
|
||||
// we extract Allay and "allay"
|
||||
const regex = line.match(/EntityType<(.*)> (.*) = register\(\W*"([a-z0-9_]+)"/)
|
||||
if (regex) {
|
||||
const [, type, , name] = regex
|
||||
classNameTo[type] = name
|
||||
nameToClass[name] = type
|
||||
}
|
||||
}
|
||||
}
|
||||
return [classNameTo, nameToClass]
|
||||
}
|
||||
|
||||
/**
|
||||
* Read EntityDataSerializers.java and return serializer names in order.
|
||||
* @param {string} versionDir
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function getEntityMetadataSerializers (versionDir) {
|
||||
const output = []
|
||||
const entityTypes = fs.readFileSync(`${versionDir}/client/net/minecraft/network/syncher/EntityDataSerializers.java`, 'utf8')
|
||||
const entityTypesLines = prepLines(entityTypes)
|
||||
for (const line of entityTypesLines) {
|
||||
if (line.includes('registerSerializer')) {
|
||||
// From: registerSerializer(BYTE);
|
||||
// We extract: BYTE
|
||||
const regex = line.match(/ {2}registerSerializer\((.*)\)/)
|
||||
if (regex) {
|
||||
const [, type] = regex
|
||||
output.push(type)
|
||||
}
|
||||
}
|
||||
}
|
||||
return output.map(e => e.toLowerCase())
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to extract and optionally update mcdata files.
|
||||
* @param {string} version - directory name for extracted src (e.g. "1.20")
|
||||
* @param {string} [mcdataVersion] - mcdata version to update (defaults to version)
|
||||
* @param {{write?:boolean, cloneIfMissing?:boolean}} [opts]
|
||||
* @returns {{tree:object, flat:object, serializers:string[], classNameToRegistryName:object, entityNameToClass:object, metadatas:object}}
|
||||
*/
|
||||
function extractPcEntityMetadata (version, mcdataVersion = version, opts = {}) {
|
||||
if (!version) {
|
||||
throw new Error('Usage: extractPcEntityMetadata(version, [mcdataVersion])')
|
||||
}
|
||||
const write = opts.write !== false
|
||||
const cloneIfMissing = opts.cloneIfMissing !== false
|
||||
|
||||
if (!fs.existsSync(version)) {
|
||||
if (!cloneIfMissing) {
|
||||
throw new Error(`Version directory "${version}" does not exist`)
|
||||
}
|
||||
cp.execSync(`git clone -b client${version} https://github.com/extremeheat/extracted_minecraft_data.git ${version} --depth 1`, { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
const serializers = getEntityMetadataSerializers(version)
|
||||
const [classNameToRegistryName, entityNameToClass] = getEntityTypes(version)
|
||||
|
||||
const allEntityFiles = globSync(`${version}/**/entity/**/*.java`)
|
||||
const allEntityFileCodes = Object.fromEntries(allEntityFiles.map(file => [
|
||||
file.split(/\\|\//g).pop().replace('.java', ''),
|
||||
fs.readFileSync(file, 'utf8')
|
||||
]))
|
||||
|
||||
const entityPreTree = []
|
||||
const metadatas = {}
|
||||
|
||||
for (const file in allEntityFileCodes) {
|
||||
const code = allEntityFileCodes[file]
|
||||
const lines = prepLines(code)
|
||||
let lastClass
|
||||
for (const line of lines) {
|
||||
let lineWithoutGenerics = line.replace(/<.*>/g, '')
|
||||
const bloatMods = ['abstract', 'static']
|
||||
for (const mod of bloatMods) lineWithoutGenerics = lineWithoutGenerics.replace(` ${mod} `, ' ')
|
||||
|
||||
if (lineWithoutGenerics.includes('public class ')) {
|
||||
const className = lineWithoutGenerics.split('public class ')[1]?.split(' ')[0]
|
||||
lastClass = className
|
||||
const extend = lineWithoutGenerics.split('extends ')[1]?.split(' ')[0]
|
||||
if (className === file) {
|
||||
if (extend) entityPreTree.push([extend, file])
|
||||
else entityPreTree.push([file])
|
||||
} else if (extend === file) {
|
||||
lastClass = `${file}.${className}`
|
||||
entityPreTree.push([file, lastClass])
|
||||
}
|
||||
}
|
||||
|
||||
if (line.match(/SynchedEntityData\..*defineId\(/)) {
|
||||
// from: private static final EntityDataAccessor<Sniffer.State> DATA_STATE = SynchedEntityData.defineId(Sniffer.class, EntityDataSerializers.SNIFFER_STATE);
|
||||
// extract: DATA_STATE, SNIFFER_STATE
|
||||
const r = line.match(/> ([A-Z_0-9]+) = .*EntityDataSerializers.([A-Z_0-9]+)/s)
|
||||
if (r) {
|
||||
const [, data, serializer] = r
|
||||
; (metadatas[lastClass] ??= []).push([data, serializer])
|
||||
} else {
|
||||
throw new Error('Failed to parse line: ' + line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tree = {}
|
||||
const flat = {}
|
||||
const handledNames = []
|
||||
function build (ele, root = tree, arr = []) {
|
||||
const name = classNameToRegistryName[ele]
|
||||
const metadata = metadatas[ele]
|
||||
root[ele] = { children: {}, name, metadata }
|
||||
if (name) flat[name] = arr.concat(metadata ?? [])
|
||||
handledNames.push(ele)
|
||||
for (const [key, val] of entityPreTree) {
|
||||
if (key === ele) {
|
||||
build(val, root[key].children, arr.concat(metadata ?? []))
|
||||
}
|
||||
}
|
||||
}
|
||||
build('Entity')
|
||||
|
||||
for (const key in classNameToRegistryName) {
|
||||
if (!handledNames.includes(key)) throw new Error('Unhandled entity ' + key)
|
||||
}
|
||||
|
||||
function updateMcDataEntitiesJSON () {
|
||||
const presentMcDataPath = `../../data/pc/${mcdataVersion}/entities.json`
|
||||
const presentMcData = require(presentMcDataPath)
|
||||
for (const entry of presentMcData) {
|
||||
const cls = entityNameToClass[entry.name]
|
||||
const regName = classNameToRegistryName[cls]
|
||||
entry.metadataKeys = (flat[regName] || []).map(e => e[0].replace('DATA_', '').replace('_ID', '').replace('ID_', '').toLowerCase())
|
||||
}
|
||||
fs.writeFileSync(presentMcDataPath, JSON.stringify(presentMcData, null, 2))
|
||||
}
|
||||
|
||||
function updateMcDataProtocolJSON () {
|
||||
const presentMcDataPath = `../../data/pc/${mcdataVersion}/protocol.json`
|
||||
const mcdata = require(presentMcDataPath)
|
||||
const mapper = { type: 'varint', mappings: {} }
|
||||
for (let i = 0; i < serializers.length; i++) {
|
||||
mapper.mappings[i] = serializers[i].toLowerCase()
|
||||
}
|
||||
mcdata.types.entityMetadata[1].type[1] = [
|
||||
{ name: 'key', type: 'u8' },
|
||||
{ name: 'type', type: ['mapper', mapper] },
|
||||
{ name: 'value', type: ['entityMetadataItem', { compareTo: 'type' }] }
|
||||
]
|
||||
fs.writeFileSync(presentMcDataPath, JSON.stringify(mcdata, null, 2))
|
||||
}
|
||||
|
||||
if (write) {
|
||||
updateMcDataEntitiesJSON()
|
||||
updateMcDataProtocolJSON()
|
||||
}
|
||||
|
||||
return {
|
||||
tree,
|
||||
flat,
|
||||
serializers,
|
||||
classNameToRegistryName,
|
||||
entityNameToClass,
|
||||
metadatas,
|
||||
entityPreTree
|
||||
}
|
||||
}
|
||||
|
||||
// Expose functions for use as a library
|
||||
module.exports = {
|
||||
extractPcEntityMetadata,
|
||||
prepLines,
|
||||
getEntityTypes,
|
||||
getEntityMetadataSerializers
|
||||
}
|
||||
|
||||
// If run directly from the CLI, call the function with process.argv
|
||||
if (require.main === module) {
|
||||
const version = process.argv[2]
|
||||
const mcdataVersion = process.argv[3] || process.argv[2]
|
||||
if (!version) {
|
||||
console.log('Usage: node extractEntityMetadata.js <codeVersion> [mcdataVersion]')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
extractPcEntityMetadata(version, mcdataVersion, { write: true, cloneIfMissing: true })
|
||||
// finished successfully
|
||||
} catch (err) {
|
||||
console.error('Error:', err)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
const fs = require('fs')
|
||||
const cp = require('child_process')
|
||||
const version = process.argv[2]
|
||||
if (!version) {
|
||||
console.log('Usage: node extractPcFoods.js <version>')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(version)) {
|
||||
cp.execSync(`git clone -b client${version} https://github.com/extremeheat/extracted_minecraft_data.git ${version} --depth 1`, { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
const mcDataItems = require(`../../data/pc/${version}/items.json`)
|
||||
const outputItems = []
|
||||
function getItemDataFor (itemName) {
|
||||
return mcDataItems.find(e => e.name === itemName)
|
||||
}
|
||||
|
||||
const foodsFile = fs.readFileSync(`./${version}/client/net/minecraft/world/food/Foods.java`, 'utf8')
|
||||
|
||||
function add (name, satMod, nut) {
|
||||
console.log(name, satMod, nut)
|
||||
// for correct order, we assign undefineds
|
||||
const entry = {
|
||||
id: undefined,
|
||||
name: name.toLowerCase(),
|
||||
stackSize: null,
|
||||
displayName: undefined,
|
||||
foodPoints: parseFloat(nut),
|
||||
saturation: undefined,
|
||||
effectiveQuality: undefined,
|
||||
// There is a ratio*2 modifier as of 1.20.5 in https://github.com/extremeheat/extracted_minecraft_data/blob/dc3974eeb6bea61fb26f8f1feece761d0b812d6a/client/net/minecraft/world/food/FoodConstants.java#L35
|
||||
// also done in https://github.com/PrismarineJS/minecraft-data-generator-server/blob/44f49b4ab51fb92f72209d7fdf7c6e42030de8dd/1.7/src/main/java/dev/u9g/minecraftdatagenerator/generators/FoodsDataGenerator.java#L24
|
||||
saturationRatio: parseFloat(satMod).toFixed(2) * 2
|
||||
}
|
||||
Object.assign(entry, getItemDataFor(entry.name))
|
||||
entry.saturation = Number((entry.foodPoints * entry.saturationRatio).toFixed(2)) // round by 2 decimal digits
|
||||
entry.effectiveQuality = entry.foodPoints + entry.saturation
|
||||
outputItems.push(entry)
|
||||
}
|
||||
|
||||
for (const line of foodsFile.replaceAll('\n', '').split(';')) {
|
||||
if (!line.includes('final FoodProperties')) continue
|
||||
if (line.includes('stew(')) {
|
||||
// special case for stew
|
||||
const name = line.match(/ ([A-Z0-9+_]+) =/)[1]
|
||||
const satMod = 0.6
|
||||
const nut = line.match(/stew\(([0-9.fFeE]+)\)/)[1]
|
||||
add(name, satMod, nut)
|
||||
} else {
|
||||
const name = line.match(/ ([A-Z0-9+_]+) =/)[1]
|
||||
const satMod = line.match(/saturationModifier\(([0-9.fFeE]+)\)/)[1]
|
||||
const nut = line.match(/nutrition\(([0-9.fFeE]+)\)/)[1]
|
||||
add(name, satMod, nut)
|
||||
}
|
||||
}
|
||||
outputItems.sort((a, b) => a.id - b.id)
|
||||
fs.writeFileSync(`../../data/pc/${version}/foods.json`, JSON.stringify(outputItems, null, 2))
|
||||
@@ -0,0 +1,23 @@
|
||||
const fs = require('fs')
|
||||
const cp = require('child_process')
|
||||
const version = process.argv[2]
|
||||
const extractTypes = process.argv[3] === '-types'
|
||||
|
||||
if (!version) {
|
||||
console.log('Usage: node extractEntityMetadata.js <codeVersion> [-types]')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
if (!fs.existsSync(version)) {
|
||||
cp.execSync(`git clone -b client${version} https://github.com/extremeheat/extracted_minecraft_data.git ${version} --depth 1`, { stdio: 'inherit' })
|
||||
}
|
||||
|
||||
const componentsFile = fs.readFileSync(`./${version}/client/net/minecraft/core/component/DataComponents.java`, 'utf8')
|
||||
const nameRe = /register\("(.+)",/gm
|
||||
const typeRe = /\((.+?)\).*?;/gm
|
||||
const nameMatches = componentsFile.matchAll(nameRe)
|
||||
const typeMatches = componentsFile.matchAll(typeRe)
|
||||
typeMatches.next()
|
||||
for (const match of nameMatches) {
|
||||
console.log(`- ${match[1]}${extractTypes ? ' : ' + typeMatches.next().value?.[1] : ''}`)
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
const fs = require('fs')
|
||||
const { join } = require('path')
|
||||
|
||||
const rootPath = join(__dirname, '..', '..')
|
||||
const data = join(__dirname, '..', '..', 'data')
|
||||
|
||||
function getJSON (path) {
|
||||
return JSON.parse(fs.readFileSync(path, 'utf-8'))
|
||||
}
|
||||
|
||||
function writeJSON (path, data) {
|
||||
fs.writeFileSync(path, JSON.stringify(data, null, 2), 'utf-8')
|
||||
}
|
||||
|
||||
// Reads and updates the JSON after processing in callback
|
||||
function alterJSON (path, callback) {
|
||||
const jsonContents = getJSON(path)
|
||||
if (callback(jsonContents) !== false) {
|
||||
writeJSON(path, jsonContents)
|
||||
}
|
||||
}
|
||||
|
||||
function bumpReadmeVersion (edition, version) {
|
||||
const readmePath = join(rootPath, 'README.md')
|
||||
let readmeContents = fs.readFileSync(readmePath, 'utf-8')
|
||||
if (!readmeContents.includes(version)) {
|
||||
if (edition === 'pc') readmeContents = readmeContents.replace('\n<!--NEXT PC-->', `, ${version}\n<!--NEXT PC-->`)
|
||||
if (edition === 'bedrock') readmeContents = readmeContents.replace('\n<!--NEXT BEDROCK-->', `, ${version}\n<!--NEXT BEDROCK-->`)
|
||||
}
|
||||
fs.writeFileSync(readmePath, readmeContents, 'utf-8')
|
||||
}
|
||||
|
||||
function updateProtocol (edition, version, protocolVersionNumber) {
|
||||
const dataRoot = join(data, edition)
|
||||
const protocolVersionsPath = join(dataRoot, 'common', 'protocolVersions.json')
|
||||
const versionsPath = join(dataRoot, 'common', 'versions.json')
|
||||
const versionPath = join(dataRoot, version, 'version.json')
|
||||
|
||||
const protocolVersionsEntry = {
|
||||
version: parseInt(protocolVersionNumber),
|
||||
minecraftVersion: version,
|
||||
majorVersion: version.split('.').slice(0, 2).join('.'),
|
||||
releaseType: 'release'
|
||||
}
|
||||
|
||||
let didUpdateProtocol = false
|
||||
|
||||
// Move the latest/proto.yml to its associated folder defined in the !version meta field
|
||||
const latestProtoPath = join(dataRoot, 'latest', 'proto.yml')
|
||||
const protoYml = fs.readFileSync(latestProtoPath, 'utf-8')
|
||||
const oldProtoVersion = protoYml.match(/!version: ([0-9.]+)/)[1]
|
||||
// Check if $oldVersion/proto.yml already exists
|
||||
const oldProtoPath = join(dataRoot, oldProtoVersion, 'proto.yml')
|
||||
if (fs.existsSync(oldProtoPath)) {
|
||||
console.log(`Can't move ${latestProtoPath} to ${oldProtoPath} because the file already exists.`)
|
||||
} else {
|
||||
fs.copyFileSync(latestProtoPath, oldProtoPath)
|
||||
if (edition === 'bedrock') {
|
||||
fs.copyFileSync(join(dataRoot, 'latest', 'types.yml'), join(dataRoot, oldProtoVersion, 'types.yml'))
|
||||
}
|
||||
didUpdateProtocol = true
|
||||
// Update references to the old version in the dataPaths.json file
|
||||
alterJSON(join(data, 'dataPaths.json'), dataPaths => {
|
||||
let latestVersionData
|
||||
for (const v in dataPaths[edition]) {
|
||||
const e = dataPaths[edition][v]
|
||||
// if .proto points to latest, update it to the old version
|
||||
if (e.proto === `${edition}/latest`) {
|
||||
e.proto = `${edition}/${oldProtoVersion}`
|
||||
}
|
||||
latestVersionData = structuredClone(e) // eslint-disable-line
|
||||
}
|
||||
if (!dataPaths[edition][version]) {
|
||||
dataPaths[edition][version] = latestVersionData
|
||||
}
|
||||
latestVersionData.proto = `${edition}/latest`
|
||||
latestVersionData.protocol = `${edition}/${version}`
|
||||
latestVersionData.version = `${edition}/${version}`
|
||||
})
|
||||
}
|
||||
|
||||
// Update commons/protocolVersions.json (pc does this auto with cron)
|
||||
alterJSON(protocolVersionsPath, protoVers => {
|
||||
if (!protoVers.find(v => v.version === protocolVersionsEntry.version)) {
|
||||
protoVers.unshift(protocolVersionsEntry)
|
||||
}
|
||||
})
|
||||
// Update commons/version.json
|
||||
alterJSON(versionsPath, versions => {
|
||||
if (!versions.includes(version)) {
|
||||
versions.push(version)
|
||||
}
|
||||
})
|
||||
|
||||
// Update the $version/version.json file
|
||||
fs.mkdirSync(join(dataRoot, version), { recursive: true })
|
||||
if (!fs.existsSync(versionPath)) {
|
||||
writeJSON(versionPath, protocolVersionsEntry)
|
||||
}
|
||||
|
||||
// Update the '!version' meta field in the latest/proto.yml
|
||||
if (didUpdateProtocol) {
|
||||
const protoYmlUpdate = protoYml.replace(/!version: [0-9.]+/, `!version: ${version}`)
|
||||
fs.writeFileSync(join(dataRoot, 'latest', 'proto.yml'), protoYmlUpdate, 'utf-8')
|
||||
}
|
||||
}
|
||||
|
||||
const platforms = {
|
||||
bedrock: updateProtocol,
|
||||
pc: updateProtocol
|
||||
}
|
||||
|
||||
if (process.argv.length !== 5) {
|
||||
console.log(`Usage: npm run version <${Object.keys(platforms).join('|')}> {version} {protocol_version}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const [,, platform, version, protocol] = process.argv
|
||||
|
||||
if (platforms[platform] === undefined) {
|
||||
console.log(`Received platform "${platform}", expected one of (${Object.keys(platforms).join(', ')})`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
platforms[platform](platform, version, protocol)
|
||||
bumpReadmeVersion(platform, version)
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "minecraft-data-tools",
|
||||
"private": true,
|
||||
"description": "minecraft-data tools",
|
||||
"scripts": {
|
||||
"test": "mocha --exit --reporter spec",
|
||||
"lint": "standard && standard ../../.github",
|
||||
"fix": "standard --fix && standard --fix ../../.github",
|
||||
"pretest": "npm run lint",
|
||||
"build": "node ./compileProtocol.js",
|
||||
"version": "node ./incrementVersion.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": "^6.5.4",
|
||||
"glob": "^10.2.6",
|
||||
"mocha": "^10.2.0",
|
||||
"protodef-validator": "^1.2.2",
|
||||
"standard": "^17.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"protodef-yaml": "^1.5.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Sync particle IDs between pc/particles.json and pc/protocol.json
|
||||
const fs = require('fs')
|
||||
const oldParticleData = require('../../data/pc/1.19.3/particles.json')
|
||||
const oldProtocolData = require('../../data/pc/1.19.3/protocol.json')
|
||||
const particleData = require('../../data/pc/1.19.4/particles.json')
|
||||
const protocolData = require('../../data/pc/1.19.4/protocol.json')
|
||||
|
||||
// const oldParticleDataByName = Object.fromEntries(oldParticleData.map(e => [e.name, e.id]))
|
||||
const newParticleDataByName = Object.fromEntries(particleData.map(e => [e.name, e.id]))
|
||||
const oldIdToNew = Object.fromEntries(oldParticleData.map(e => [e.id, newParticleDataByName[e.name]]))
|
||||
|
||||
const ppData = oldProtocolData.types.particleData[1]
|
||||
const newFields = {}
|
||||
for (const fieldId in ppData.fields) {
|
||||
const field = ppData.fields[fieldId]
|
||||
newFields[oldIdToNew[fieldId]] = field
|
||||
}
|
||||
|
||||
protocolData.types.particleData[1].fields = newFields
|
||||
const json = JSON.stringify(protocolData, null, 2)
|
||||
// Matching the custom formatting on the JSON is annoying, let's just keep it JS/vscode default 2-space formatting for consistency
|
||||
// json = json.replace(/\[\s+"option",\s+"string"\s+\],/g, '["option", "string"],').replace(/\[\s+"option",\s+"string"\s+\]/g, '["option", "string"]')
|
||||
fs.writeFileSync('./protocol.json', json)
|
||||
@@ -0,0 +1,48 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
require('./version_iterator')(function (p, versionString) {
|
||||
describe('audit blockitems ' + versionString, function () {
|
||||
it('audit blockitems', function () {
|
||||
const itemsFile = path.join(p, 'items.json')
|
||||
if (!fs.existsSync(itemsFile)) return
|
||||
const items = require(itemsFile)
|
||||
|
||||
const blocksFile = path.join(p, 'blocks.json')
|
||||
if (!fs.existsSync(blocksFile)) return
|
||||
const blocks = require(blocksFile)
|
||||
|
||||
if (items[0].id === 0) return
|
||||
|
||||
let rewriteItems = false
|
||||
for (const block of blocks) {
|
||||
let blockitem = null
|
||||
for (const item of items) {
|
||||
if (item.name === block.name) {
|
||||
blockitem = item
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!blockitem) {
|
||||
// console.log('Missing item for block ' + block.name)
|
||||
rewriteItems = false
|
||||
items.push({
|
||||
id: block.id,
|
||||
displayName: block.displayName,
|
||||
name: block.name,
|
||||
stackSize: 64
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
items.sort((a, b) => { return a.id - b.id })
|
||||
|
||||
// Automatically fix item data, if necessary
|
||||
if (rewriteItems) {
|
||||
fs.writeFileSync(itemsFile, JSON.stringify(items, null, 2))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// checks for duplicates names and jumps in ids
|
||||
|
||||
require('./version_iterator')(function (p, versionString) {
|
||||
describe('audit blocks ' + versionString, function () {
|
||||
it('audit blocks', function () {
|
||||
let blocks
|
||||
const pFile = path.join(p, 'blocks.json')
|
||||
if (fs.existsSync(pFile)) {
|
||||
blocks = require(pFile)
|
||||
} else {
|
||||
// console.log('No blocks for version ' + versionString)
|
||||
}
|
||||
if (blocks) {
|
||||
const all = []
|
||||
blocks.forEach(block => {
|
||||
all[block.id] = block
|
||||
})
|
||||
|
||||
const displayNames = {}
|
||||
const names = {}
|
||||
for (let i = 0; i < all.length; ++i) {
|
||||
const block = all[i]
|
||||
if (block) {
|
||||
if (block.displayName == null) {
|
||||
// console.log('Missing displayName:', i)
|
||||
} else {
|
||||
const otherBlock = displayNames[block.displayName]
|
||||
if (otherBlock) {
|
||||
// console.log('Duplicate displayName:', otherBlock.id, 'and', block.id,
|
||||
// 'both share', block.displayName)
|
||||
} else {
|
||||
displayNames[block.displayName] = block
|
||||
}
|
||||
}
|
||||
if (block.name == null) {
|
||||
console.log('Missing name:', i)
|
||||
} else {
|
||||
const otherBlock = names[block.name]
|
||||
if (otherBlock) {
|
||||
// console.log('Duplicate name:', otherBlock.id, 'and', block.id,
|
||||
// 'both share', block.name)
|
||||
} else {
|
||||
names[block.name] = block
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// console.log('Missing:', i)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
const fs = require('fs')
|
||||
const { join } = require('path')
|
||||
const assert = require('assert')
|
||||
|
||||
describe('audit dataPaths', function () {
|
||||
const dataPaths = require('../../../data/dataPaths.json')
|
||||
it('should have a dataPath for each file', function () {
|
||||
require('./version_iterator')(function (p, versionString) {
|
||||
const [type, version] = versionString.split(' ')
|
||||
const dp = dataPaths[type][version]
|
||||
if (fs.existsSync(p)) {
|
||||
const files = fs.readdirSync(p).map(f => f.split('.')[0])
|
||||
for (const file of files) {
|
||||
assert(dp[file], `missing dataPath for ${type} ${version} ${file}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('dataPath should point to valid files', function () {
|
||||
for (const version in dataPaths) {
|
||||
for (const type in dataPaths[version]) {
|
||||
const dp = dataPaths[version][type]
|
||||
for (const file in dp) {
|
||||
const path = dp[file]
|
||||
const p = join(__dirname, '../../../data/' + path + '/' + file)
|
||||
const exists = fs.existsSync(p + '.json') || fs.existsSync(p + '.yml')
|
||||
assert(exists, `missing file for ${type} ${version} ${file}, path: ${p}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// checks for duplicates names and jumps in ids
|
||||
|
||||
require('./version_iterator')(function (p, versionString) {
|
||||
describe('audit items ' + versionString, function () {
|
||||
it('audit items', function () {
|
||||
let items
|
||||
const pFile = path.join(p, 'items.json')
|
||||
if (fs.existsSync(pFile)) {
|
||||
items = require(pFile)
|
||||
} else {
|
||||
// console.log('No items for version ' + versionString)
|
||||
}
|
||||
if (items) {
|
||||
const displayNames = {}
|
||||
const names = {}
|
||||
let lastItemId = 0
|
||||
items.forEach(item => {
|
||||
if (item.displayName == null) {
|
||||
console.log('Missing displayName:', item.id)
|
||||
} else {
|
||||
const otherBlock = displayNames[item.displayName]
|
||||
if (otherBlock) {
|
||||
// console.log('Duplicate displayName:', otherBlock.id, 'and', item.id,
|
||||
// 'both share', item.displayName)
|
||||
} else {
|
||||
displayNames[item.displayName] = item
|
||||
}
|
||||
}
|
||||
if (item.name == null) {
|
||||
console.log('Missing name:', item.id)
|
||||
} else {
|
||||
const otherBlock = names[item.name]
|
||||
if (otherBlock) {
|
||||
// console.log('Duplicate name:', otherBlock.id, 'and', item.id,
|
||||
// 'both share', item.name)
|
||||
} else {
|
||||
names[item.name] = item
|
||||
}
|
||||
}
|
||||
const delta = item.id - lastItemId
|
||||
if (delta !== 1) {
|
||||
// console.log('jump from', lastItemId, 'to', item.id)
|
||||
}
|
||||
lastItemId = item.id
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,136 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const assert = require('assert')
|
||||
|
||||
// counts the number of recipes with a shape, without one and with an outShape
|
||||
|
||||
function getIfExist (path) {
|
||||
if (fs.existsSync(path)) {
|
||||
return require(path)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
require('./version_iterator')(function (p, versionString) {
|
||||
describe('audit recipes ' + versionString, function () {
|
||||
it('audit recipes', function () {
|
||||
let recipes
|
||||
const pFile = path.join(p, 'recipes.json')
|
||||
if (fs.existsSync(pFile)) {
|
||||
recipes = require(pFile)
|
||||
} else {
|
||||
console.log('No recipes for version ' + versionString)
|
||||
}
|
||||
if (recipes) {
|
||||
let shapeCount = 0
|
||||
let shapelessCount = 0
|
||||
let outShapeCount = 0
|
||||
Object.keys(recipes).forEach(key => {
|
||||
const list = recipes[key]
|
||||
for (let i = 0; i < list.length; ++i) {
|
||||
const recipe = list[i]
|
||||
if (recipe.inShape != null) {
|
||||
shapeCount += 1
|
||||
} else if (recipe.ingredients != null) {
|
||||
shapelessCount += 1
|
||||
} else {
|
||||
console.log('inShape or ingredients required:', key)
|
||||
}
|
||||
if (recipe.outShape) outShapeCount += 1
|
||||
}
|
||||
})
|
||||
|
||||
console.log('normal recipes:', shapeCount)
|
||||
console.log('shapeless recipes:', shapelessCount)
|
||||
console.log('how many have an outShape:', outShapeCount)
|
||||
}
|
||||
})
|
||||
it('pickaxe not upside-down', () => {
|
||||
const recipes = getIfExist(path.join(p, 'recipes.json'))
|
||||
const items = getIfExist(path.join(p, 'items.json'))
|
||||
if (recipes && items) {
|
||||
const pickaxe = items.find(x => x.name === 'diamond_pickaxe')
|
||||
const stick = items.find(x => x.name === 'stick')
|
||||
const diamond = items.find(x => x.name === 'diamond')
|
||||
|
||||
const recipe = recipes[pickaxe.id]
|
||||
|
||||
// Uncomment to fix upside-down recipes
|
||||
/* if (recipe[0].inShape[0][0] !== diamond.id) {
|
||||
for (const item of Object.values(recipes)) {
|
||||
for (const rep of item) {
|
||||
if (rep.inShape) rep.inShape.reverse()
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(path.join(p, 'recipes.json'), JSON.stringify(recipes, null, 2))
|
||||
} */
|
||||
|
||||
// Those 2 versions doesnt contain diamond pickaxe recipe, to be fixed...
|
||||
if (versionString === 'pc 1.9' || versionString === 'pc 1.10' || !recipe[0]) return
|
||||
|
||||
assert.deepStrictEqual(recipe[0].inShape, [
|
||||
[
|
||||
diamond.id,
|
||||
diamond.id,
|
||||
diamond.id
|
||||
],
|
||||
[
|
||||
null,
|
||||
stick.id,
|
||||
null
|
||||
],
|
||||
[
|
||||
null,
|
||||
stick.id,
|
||||
null
|
||||
]
|
||||
])
|
||||
}
|
||||
})
|
||||
it('iron door not rotated', () => {
|
||||
const recipes = getIfExist(path.join(p, 'recipes.json'))
|
||||
const items = getIfExist(path.join(p, 'items.json'))
|
||||
if (recipes && items) {
|
||||
const ironDoor = items.find(x => x.name === 'iron_door')
|
||||
const iron = items.find(x => x.name === 'iron_ingot')
|
||||
|
||||
const recipe = recipes[ironDoor.id]
|
||||
|
||||
if (!recipe[0]) return
|
||||
|
||||
assert.deepStrictEqual(recipe[0].inShape, [
|
||||
[
|
||||
iron.id,
|
||||
iron.id
|
||||
],
|
||||
[
|
||||
iron.id,
|
||||
iron.id
|
||||
],
|
||||
[
|
||||
iron.id,
|
||||
iron.id
|
||||
]
|
||||
])
|
||||
}
|
||||
})
|
||||
it('crafting benches has multiple recipes', () => {
|
||||
const recipes = getIfExist(path.join(p, 'recipes.json'))
|
||||
const items = getIfExist(path.join(p, 'items.json'))
|
||||
if (recipes && items) {
|
||||
const craftingTable = items.find(x => x.name === 'crafting_table')
|
||||
const oakPlanks = items.find(x => x.name === 'oak_planks')
|
||||
if (!oakPlanks) return // Bail if version doesn't have seperately defined planks, this prevents the test failing on versions that use metadata
|
||||
const recipe = recipes[craftingTable.id]
|
||||
if (!recipe[0]) return
|
||||
// remove the if after fixing https://github.com/PrismarineJS/minecraft-data/issues/917
|
||||
if (versionString !== 'pc 1.20.5' && versionString !== 'pc 1.20.6') {
|
||||
assert.notEqual(recipe.length, 1) // Check that crafting table has multiple recipes.
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,94 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
// checks if bounding boxes data in blocks.json is consistent with blockCollisionShapes.json
|
||||
|
||||
require('./version_iterator')(function (p, versionString) {
|
||||
describe('audit block shapes ' + versionString, function () {
|
||||
const blockFile = path.join(p, 'blocks.json')
|
||||
if (!fs.existsSync(blockFile)) {
|
||||
return // Only audit shape where block exists
|
||||
}
|
||||
it('audit bb', function () {
|
||||
const shapeFile = path.join(p, 'blockCollisionShapes.json')
|
||||
if (!fs.existsSync(shapeFile)) {
|
||||
// console.log('No collision shapes for version ' + versionString)
|
||||
return
|
||||
}
|
||||
|
||||
const blocks = require(blockFile)
|
||||
const shapes = require(shapeFile)
|
||||
|
||||
const blockByName = {}
|
||||
|
||||
// Check bijection between blocks and shapes
|
||||
|
||||
// Check that every block have a shape
|
||||
blocks.forEach(block => {
|
||||
blockByName[block.name] = block
|
||||
const shape = shapes.blocks[block.name]
|
||||
if (shape === undefined) {
|
||||
// console.log('missing shape for block: ' + block.name)
|
||||
}
|
||||
})
|
||||
|
||||
// Check that every shape have a block
|
||||
for (const key of Object.keys(shapes.blocks)) {
|
||||
const block = blockByName[key]
|
||||
if (block === undefined) {
|
||||
// console.log('missing block for shape: ' + key)
|
||||
}
|
||||
}
|
||||
|
||||
// Check every shape is present
|
||||
const usedShapes = {}
|
||||
for (let value of Object.values(shapes.blocks)) {
|
||||
if (!(value instanceof Array)) value = [value]
|
||||
for (const shape of value) {
|
||||
if (shapes.shapes[shape] === undefined) {
|
||||
// console.log('missing shape: ' + shape)
|
||||
}
|
||||
usedShapes[shape] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Check every shape is used
|
||||
for (const key of Object.keys(shapes.shapes)) {
|
||||
if (!usedShapes[key]) {
|
||||
// console.log('unused shape: ' + key)
|
||||
}
|
||||
}
|
||||
|
||||
// Check block bounding box is consistent with shape
|
||||
let rewriteBlocks = false
|
||||
blocks.forEach(block => {
|
||||
blockByName[block.name] = block
|
||||
const shape = shapes.blocks[block.name]
|
||||
if (shape !== undefined) {
|
||||
if (block.boundingBox === 'empty') {
|
||||
if (shape !== 0) {
|
||||
// console.log('Inconsistent BB for block ' + block.name + ' (expected empty got ' + shape + ')')
|
||||
block.boundingBox = 'block'
|
||||
rewriteBlocks = false
|
||||
}
|
||||
} else if (block.boundingBox === 'block') {
|
||||
if (shape === 0) {
|
||||
// console.log('Inconsistent BB for block ' + block.name + ' (expected block got ' + shape + ')')
|
||||
block.boundingBox = 'empty'
|
||||
rewriteBlocks = false
|
||||
}
|
||||
} else {
|
||||
// console.log('Unknown BB: ' + block.boundingBox + ' for block ' + block.name)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Automatically fix block data, if necessary
|
||||
if (rewriteBlocks) {
|
||||
fs.writeFileSync(blockFile, JSON.stringify(blocks, null, 2))
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,95 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
const assert = require('assert')
|
||||
|
||||
/**
|
||||
* Gets the chronological index of a version from the versions.json file
|
||||
* Returns -1 if version is not found, or null for non-version strings like "latest"
|
||||
*/
|
||||
function getVersionIndex (platform, version) {
|
||||
// Skip non-version entries like "latest"
|
||||
if (version === 'latest') return null
|
||||
|
||||
try {
|
||||
const versions = require(`../../../data/${platform}/common/versions.json`)
|
||||
return versions.indexOf(version)
|
||||
} catch (error) {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
describe('audit version regression in dataPaths', function () {
|
||||
const dataPaths = require('../../../data/dataPaths.json')
|
||||
|
||||
const platforms = ['pc', 'bedrock']
|
||||
|
||||
platforms.forEach(function (platform) {
|
||||
if (!dataPaths[platform]) return
|
||||
|
||||
describe(`${platform} version regression`, function () {
|
||||
it('should not have newer versions pointing to older data than previously seen', function () {
|
||||
const platformData = dataPaths[platform]
|
||||
|
||||
// Load the chronological version order from versions.json
|
||||
const versions = require(`../../../data/${platform}/common/versions.json`)
|
||||
|
||||
// Track the highest version index seen for each data type
|
||||
const highestVersionIndexForDataType = {}
|
||||
|
||||
// Iterate through versions in chronological order, but only process ones in dataPaths
|
||||
for (const version of versions) {
|
||||
if (!platformData[version]) continue
|
||||
|
||||
const versionData = platformData[version]
|
||||
|
||||
for (const [dataType, dataPath] of Object.entries(versionData)) {
|
||||
// Skip non-version data paths (like "latest" or cross-platform refs)
|
||||
const pathParts = dataPath.split('/')
|
||||
if (pathParts.length !== 2) continue
|
||||
|
||||
const [dataPathPlatform, dataPathVersion] = pathParts
|
||||
|
||||
// Skip cross-platform references (e.g., bedrock data pointing to pc data)
|
||||
if (dataPathPlatform !== platform) continue
|
||||
|
||||
// Get the chronological index of the data path version
|
||||
const dataPathIndex = getVersionIndex(platform, dataPathVersion)
|
||||
|
||||
// Skip if version not found in versions.json (like "latest")
|
||||
if (dataPathIndex === null || dataPathIndex === -1) continue
|
||||
|
||||
// Initialize tracking for this data type if not seen before
|
||||
if (!highestVersionIndexForDataType[dataType]) {
|
||||
highestVersionIndexForDataType[dataType] = {
|
||||
version: dataPath,
|
||||
index: dataPathIndex
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if current data path is older than the highest we've seen
|
||||
const previousHighest = highestVersionIndexForDataType[dataType]
|
||||
if (dataPathIndex < previousHighest.index) {
|
||||
assert.fail(
|
||||
`Version regression detected in ${platform}:\n` +
|
||||
` Version: ${version}\n` +
|
||||
` Data type: ${dataType}\n` +
|
||||
` Current path: ${dataPath} (index: ${dataPathIndex})\n` +
|
||||
` Expected at least: ${previousHighest.version} (index: ${previousHighest.index})\n` +
|
||||
' A newer version should not point to older data than previously seen.'
|
||||
)
|
||||
}
|
||||
|
||||
// Update the highest version if current is newer
|
||||
if (dataPathIndex > previousHighest.index) {
|
||||
highestVersionIndexForDataType[dataType] = {
|
||||
version: dataPath,
|
||||
index: dataPathIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,48 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
const assert = require('assert')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
describe('dataPaths.json consistency with versions.json', function () {
|
||||
const dataPathsFile = path.join(__dirname, '../../../data/dataPaths.json')
|
||||
const dataPaths = require(dataPathsFile)
|
||||
|
||||
const editions = ['pc', 'bedrock']
|
||||
|
||||
editions.forEach(function (edition) {
|
||||
describe(`${edition} versions consistency`, function () {
|
||||
const versionsFile = path.join(
|
||||
__dirname,
|
||||
`../../../data/${edition}/common/versions.json`
|
||||
)
|
||||
assert.ok(
|
||||
fs.existsSync(versionsFile),
|
||||
`Missing versions.json for ${edition}`
|
||||
)
|
||||
|
||||
const versionsList = require(versionsFile)
|
||||
const manifestVersions = Object.keys(dataPaths[edition] || {})
|
||||
|
||||
it('all manifest versions are present in versions.json', function () {
|
||||
const missing = manifestVersions.filter(
|
||||
(v) => !versionsList.includes(v)
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
missing,
|
||||
[],
|
||||
`The following ${edition} versions are in dataPaths.json but not in versions.json: ${missing.join(', ')}`
|
||||
)
|
||||
})
|
||||
|
||||
it('no extra versions exist in versions.json compared to manifest', function () {
|
||||
const extra = versionsList.filter((v) => !manifestVersions.includes(v))
|
||||
assert.deepStrictEqual(
|
||||
extra,
|
||||
[],
|
||||
`The following ${edition} versions are in versions.json but not in dataPaths.json: ${extra.join(', ')}`
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
/* eslint-env mocha */
|
||||
const protocolTools = require('../compileProtocol')
|
||||
|
||||
describe('protocol yaml', () => {
|
||||
it('should be valid and in sync with json', () => {
|
||||
protocolTools.all(protocolTools.validate)
|
||||
}).timeout(9000)
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
const assert = require('assert')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
const Ajv = require('ajv')
|
||||
const v = new Ajv({ verbose: true })
|
||||
|
||||
const Validator = require('protodef-validator')
|
||||
|
||||
Error.stackTraceLimit = 0
|
||||
|
||||
const data = ['attributes', 'biomes', 'commands', 'instruments', 'items', 'materials', 'blocks', 'blockCollisionShapes', 'recipes', 'windows', 'entities', 'protocol', 'version', 'effects', 'enchantments', 'language', 'foods', 'particles', 'blockLoot', 'entityLoot', 'mapIcons', 'tints', 'blockMappings', 'sounds']
|
||||
|
||||
require('./version_iterator')(function (p, versionString) {
|
||||
describe('minecraft-data schemas ' + versionString, function () {
|
||||
this.timeout(60 * 1000)
|
||||
data.forEach(function (dataName) {
|
||||
let instance
|
||||
const pFile = path.join(p, dataName + '.json')
|
||||
if (fs.existsSync(pFile)) {
|
||||
instance = require(pFile)
|
||||
}
|
||||
if (instance) {
|
||||
it(dataName + '.json is valid', function () {
|
||||
// Skip tints schema validation for PC 1.21.4, as it doesn't meet the
|
||||
// maxItems: 1 check for the constant tints.
|
||||
if (dataName === 'tints' && versionString === 'pc 1.21.4') {
|
||||
this.skip()
|
||||
}
|
||||
|
||||
if (dataName === 'protocol') {
|
||||
const validator = new Validator()
|
||||
|
||||
instance.types.LatinString = 'native' // TODO: Update protodef validator
|
||||
validator.addType('entityMetadataItem', require('../../../schemas/protocol_types/entity_metadata_item.json'))
|
||||
validator.addType('entityMetadataLoop', require('../../../schemas/protocol_types/entity_metadata_loop.json'))
|
||||
validator.validateProtocol(instance)
|
||||
} else {
|
||||
const schema = require('../../../schemas/' + dataName + '_schema.json')
|
||||
const valid = v.validate(schema, instance)
|
||||
assert.ok(valid, JSON.stringify(v.errors, null, 2))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const commonData = ['protocolVersions', 'features']
|
||||
const minecraftTypes = ['pc', 'bedrock']
|
||||
|
||||
minecraftTypes.forEach(function (type) {
|
||||
describe('minecraft-data schemas of common data of ' + type, function () {
|
||||
this.timeout(60 * 1000)
|
||||
commonData.forEach(function (dataName) {
|
||||
it(dataName + '.json is valid', function () {
|
||||
const instance = require('../../../data/' + type + '/common/' + dataName + '.json')
|
||||
const schema = require('../../../schemas/' + dataName + '_schema.json')
|
||||
const valid = v.validate(schema, instance)
|
||||
assert.ok(valid, JSON.stringify(v.errors, null, 2))
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('features.json quality is good', function () {
|
||||
it('there is no duplicate feature in features.json', () => {
|
||||
const features = require('../../../data/' + type + '/common/features.json')
|
||||
const countPerFeature = {}
|
||||
for (const feature of features) {
|
||||
countPerFeature[feature.name] = countPerFeature[feature.name] ? countPerFeature[feature.name] + 1 : 1
|
||||
}
|
||||
let duplicateCount = 0
|
||||
for (const [name, count] of Object.entries(countPerFeature)) {
|
||||
if (count > 1) {
|
||||
console.log(`feature ${name} is duplicated ${count} times, please remove ${count - 1}`)
|
||||
duplicateCount += 1
|
||||
}
|
||||
}
|
||||
assert.equal(duplicateCount, 0, `${duplicateCount} duplicates found. Please remove them.`)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,14 @@
|
||||
module.exports = forEach
|
||||
|
||||
const path = require('path')
|
||||
|
||||
const minecraftTypes = ['pc', 'bedrock']
|
||||
|
||||
function forEach (f) {
|
||||
minecraftTypes.forEach(function (type) {
|
||||
const versions = require('../../../data/' + type + '/common/versions')
|
||||
versions.forEach(function (version) {
|
||||
f(path.join(__dirname, '../../../data', type, version), type + ' ' + version)
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user