Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bedrock support #86

Merged
merged 51 commits into from
Jul 22, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
44b6105
start bedrock implementation
CreeperG16 Mar 1, 2023
cc7829d
comments
CreeperG16 Mar 1, 2023
9a7634f
Todo comments
CreeperG16 Mar 2, 2023
befe982
comment
CreeperG16 Mar 2, 2023
c0a5b3e
return BedrockItem and run standard
CreeperG16 Mar 2, 2023
2e1238e
Small changes
CreeperG16 Mar 2, 2023
f8bc473
remove tools
CreeperG16 Mar 2, 2023
137af50
matchNbt in equal()
CreeperG16 Mar 2, 2023
821ea9a
better matchNbt
CreeperG16 Mar 2, 2023
f3fc3a8
remove bedrock-protocol from devDependencies
CreeperG16 Mar 2, 2023
58557ea
Notch -> Network in pc impl, add matchNbt to pc
CreeperG16 Mar 2, 2023
9e50812
canPlaceOn and canDestroy
CreeperG16 Mar 2, 2023
06269b8
CanPlaceOn and CanDestroy for java - NEED TO TEST
CreeperG16 Mar 2, 2023
a444f90
Fix weird indentation from my formatter
CreeperG16 Mar 2, 2023
5c182d8
notch -> network
CreeperG16 Mar 2, 2023
c97d5ef
revert the breaking change
CreeperG16 Mar 2, 2023
50d943f
clarify in comments that this is not tested
CreeperG16 Mar 2, 2023
397cd6b
Start bedrock support in original Item class
CreeperG16 Mar 3, 2023
c07449f
Linter
CreeperG16 Mar 3, 2023
b784243
blocksCanPlaceOn and blocksCanDestroy
CreeperG16 Mar 4, 2023
738356d
start implementing stack ID
CreeperG16 Mar 4, 2023
11b1b3e
add stack ID to fromNotch()
CreeperG16 Mar 4, 2023
20937d4
Initial support for <1.16.220, however item formats are inconsistent …
CreeperG16 Mar 5, 2023
afb0d99
<1.16.220 support, can be improved later to use supportFeature()
CreeperG16 Mar 5, 2023
e8d03a4
update types and docs
CreeperG16 Mar 5, 2023
6aaa066
update docs
CreeperG16 Mar 5, 2023
a81f906
use NBT builder functions where possible
CreeperG16 Mar 5, 2023
75c2a72
nbt.simplify() to make it more readable
CreeperG16 Mar 5, 2023
982d132
use optional chaining
CreeperG16 Mar 6, 2023
40462c3
update types
CreeperG16 Mar 6, 2023
e6a48f1
update docs
CreeperG16 Mar 6, 2023
be38e1b
Use supportFeature and bedrock features added in mcdata PR; fixes
CreeperG16 Mar 15, 2023
e1d8ea6
Add stack ID to tests (temp) and damage default to 0
CreeperG16 Mar 16, 2023
398f5c1
remove separate BedrockItem class, don't check ench len
CreeperG16 Mar 16, 2023
bdecc1a
linter
CreeperG16 Mar 16, 2023
d32d4b0
don't use Or assignment to support older node
CreeperG16 Mar 16, 2023
9a5e9cd
revert checking enchs length
CreeperG16 Mar 16, 2023
edc9aa4
use stackID parameter in tests
CreeperG16 Mar 17, 2023
b32b23a
remove network types and add stackID to fromNotch
CreeperG16 Mar 17, 2023
057f0b2
update docs and types
CreeperG16 Apr 11, 2023
bf66f42
stack ID is null in java (mcdata feature?)
CreeperG16 Apr 11, 2023
5dc0be5
Fix tests for stack ID, start adding bedrock tests
CreeperG16 Apr 11, 2023
c598975
clean up some unnecessary values
CreeperG16 Apr 11, 2023
60bbb4a
no need to test for null
CreeperG16 Apr 11, 2023
a42ce35
Merge branch 'master' into master
CreeperG16 Jul 6, 2023
96d061d
More readable notch methods
CreeperG16 Jul 18, 2023
497ae97
Change blocksCanPlaceOn/Destroy to return [name, properties]
CreeperG16 Jul 19, 2023
e3c384d
Anvil is undefined if registry type is bedrock
CreeperG16 Jul 19, 2023
4eae3bf
change blocksCanPlaceOn/Destroy
CreeperG16 Jul 19, 2023
60ec722
update types and docs
CreeperG16 Jul 19, 2023
916a7fa
Update index.d.ts
CreeperG16 Jul 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 71 additions & 20 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ function loader (registryOrVersion) {
this.displayName = itemEnum.displayName
if ('variations' in itemEnum) {
for (const i in itemEnum.variations) {
if (itemEnum.variations[i].metadata === metadata) { this.displayName = itemEnum.variations[i].displayName }
if (itemEnum.variations[i].metadata === metadata) {
this.displayName = itemEnum.variations[i].displayName
}
}
}
this.stackSize = itemEnum.stackSize
Expand All @@ -32,45 +34,51 @@ function loader (registryOrVersion) {
}
}

static equal (item1, item2, matchStackSize = true) {
static equal (item1, item2, matchStackSize = true, matchNbt = true) {
if (item1 == null && item2 == null) {
return true
} else if (item1 == null) {
return false
} else if (item2 == null) {
return false
} else {
return (item1.type === item2.type &&
(matchStackSize ? item1.count === item2.count : true) &&
item1.metadata === item2.metadata &&
JSON.stringify(item1.nbt) === JSON.stringify(item2.nbt))
return (
item1.type === item2.type &&
(matchStackSize ? item1.count === item2.count : true) &&
item1.metadata === item2.metadata &&
(matchNbt ? JSON.stringify(item1.nbt) === JSON.stringify(item2.nbt) : true)
)
}
}

static toNotch (item) {
static toNetwork (item) {
if (registry.supportFeature('itemSerializationAllowsPresent')) {
CreeperG16 marked this conversation as resolved.
Show resolved Hide resolved
if (item == null) return { present: false }
const notchItem = {
const networkItem = {
present: true,
itemId: item.type,
itemCount: item.count
}
if (item.nbt && item.nbt.length !== 0) { notchItem.nbtData = item.nbt }
return notchItem
if (item.nbt && item.nbt.length !== 0) {
networkItem.nbtData = item.nbt
}
return networkItem
} else if (registry.supportFeature('itemSerializationUsesBlockId')) {
if (item == null) return { blockId: -1 }
const notchItem = {
const networkItem = {
blockId: item.type,
itemCount: item.count,
itemDamage: item.metadata
}
if (item.nbt && item.nbt.length !== 0) { notchItem.nbtData = item.nbt }
return notchItem
if (item.nbt && item.nbt.length !== 0) {
networkItem.nbtData = item.nbt
}
return networkItem
}
throw new Error("Don't know how to serialize for this mc version ")
}

static fromNotch (item) {
CreeperG16 marked this conversation as resolved.
Show resolved Hide resolved
static fromNetwork (item) {
if (registry.supportFeature('itemSerializationWillOnlyUsePresent')) {
if (item.present === false) return null
return new Item(item.itemId, item.itemCount, item.nbtData)
Expand Down Expand Up @@ -130,7 +138,7 @@ function loader (registryOrVersion) {
} else {
itemEnch = []
}
return itemEnch.map(ench => ({ lvl: ench.lvl, name: registry.enchantments[ench.id]?.name || null }))
return itemEnch.map((ench) => ({ lvl: ench.lvl, name: registry.enchantments[ench.id]?.name || null }))
} else if (typeOfEnchantLevelValue === 'string' && enchantNbtKey === 'Enchantments') {
let itemEnch = []
if (this?.nbt?.value?.Enchantments) {
Expand All @@ -140,7 +148,10 @@ function loader (registryOrVersion) {
} else {
itemEnch = []
}
return itemEnch.map(ench => ({ lvl: ench.lvl, name: typeof ench.id === 'string' ? ench.id.replace(/minecraft:/, '') : null }))
return itemEnch.map((ench) => ({
lvl: ench.lvl,
name: typeof ench.id === 'string' ? ench.id.replace(/minecraft:/, '') : null
}))
}
throw new Error("Don't know how to get the enchants from an item on this mc version")
}
Expand All @@ -153,16 +164,25 @@ function loader (registryOrVersion) {
if (!this.nbt) this.nbt = { name: '', type: 'compound', value: {} }

const enchs = normalizedEnchArray.map(({ name, lvl }) => {
const value = type === 'short' ? registry.enchantmentsByName[name].id : `minecraft:${registry.enchantmentsByName[name].name}`
const value =
type === 'short'
? registry.enchantmentsByName[name].id
: `minecraft:${registry.enchantmentsByName[name].name}`
return { id: { type, value }, lvl: { type: 'short', value: lvl } }
})

if (enchs.length !== 0) {
this.nbt.value[isBook ? 'StoredEnchantments' : enchListName] = { type: 'list', value: { type: 'compound', value: enchs } }
this.nbt.value[isBook ? 'StoredEnchantments' : enchListName] = {
type: 'list',
value: { type: 'compound', value: enchs }
}
}

// The 'registry.itemsByName[this.name].maxDurability' checks to see if this item can lose durability
if (registry.supportFeature('whereDurabilityIsSerialized') === 'Damage' && registry.itemsByName[this.name].maxDurability) {
if (
registry.supportFeature('whereDurabilityIsSerialized') === 'Damage' &&
registry.itemsByName[this.name].maxDurability
) {
this.nbt.value.Damage = { type: 'int', value: 0 }
}
}
Expand Down Expand Up @@ -192,7 +212,7 @@ function loader (registryOrVersion) {

get spawnEggMobName () {
if (registry.supportFeature('spawnEggsUseInternalIdInNbt')) {
return registry.entitiesArray.find(o => o.internalId === this.metadata).name
return registry.entitiesArray.find((o) => o.internalId === this.metadata).name
}
if (registry.supportFeature('spawnEggsUseEntityTagInNbt')) {
const data = nbt.simplify(this.nbt)
Expand All @@ -204,6 +224,37 @@ function loader (registryOrVersion) {
}
throw new Error("Don't know how to get spawn egg mob name for this mc version")
}

// TODO: test this
canPlaceOn (block) {
if (!this.nbt || !this.nbt.value.CanPlaceOn) return true
CreeperG16 marked this conversation as resolved.
Show resolved Hide resolved
if (typeof block === 'string') {
return (
this.nbt.value.CanPlaceOn.value.includes(block) ||
this.nbt.value.CanPlaceOn.value.includes(`minecraft:${block}`)
)
} else {
return (
this.nbt.value.CanPlaceOn.value.includes(block.name) ||
this.nbt.value.CanPlaceOn.value.includes(`minecraft:${block.name}`)
)
}
}

canDestroy (block) {
if (!this.nbt || !this.nbt.value.CanPlaceOn) return true
if (typeof block === 'string') {
return (
this.nbt.value.CanDestroy.value.includes(block) ||
this.nbt.value.CanDestroy.value.includes(`minecraft:${block}`)
)
} else {
return (
this.nbt.value.CanDestroy.value.includes(block.name) ||
this.nbt.value.CanDestroy.value.includes(`minecraft:${block.name}`)
)
}
}
}

Item.anvil = require('./lib/anvil.js')(registry, Item)
Expand Down
208 changes: 208 additions & 0 deletions lib/bedrock-item.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
function loader (registryOrVersion) {
const registry = typeof registryOrVersion === 'string' ? require('prismarine-registry')(registryOrVersion) : registryOrVersion

// TODO:
// - tests
// - docs
// - add support for older versions:
// need to see how items are handled in older bedrock protocol versions,
// and probably update features.json in minecraft-data with useful data
// - Stack ID generation
// - Block runtime ID field
// - Setting canPlaceOn and canDestroy with block/item objects?
// - Merge with Item class in ../index.js?

class BedrockItem {
/**
* @param {number} type
* @param {number} count
* @param {number} metadata
* @param {object} nbt
* @param {string[]} canPlaceOnList
* @param {string[]} canDestroyList
* @param {number} stackId
* @returns {BedrockItem}
*/
constructor (type, count, metadata, nbt, canPlaceOnList, canDestroyList, stackId) {
if (type === null) return

this.type = type
this.count = count
this.metadata = metadata == null ? 0 : metadata
this.nbt = nbt || null

// TODO
// Only generate if on server side and not provided one
this.stackId = stackId ?? BedrockItem.nextStackId()

this.canPlaceOnList = canPlaceOnList ?? []
this.canDestroyList = canDestroyList ?? []

const itemEnum = registry.items[type]
if (itemEnum) {
this.name = itemEnum.name
this.displayName = itemEnum.displayName
if ('variations' in itemEnum) {
for (const variation of itemEnum.variations) {
if (variation.metadata === metadata) this.displayName = variation.displayName
}
}
this.stackSize = itemEnum.stackSize
} else {
this.name = 'unknown'
this.displayName = 'unknown'
this.stackSize = 1
}
}

/** @param {BedrockItem} item1 @param {BedrockItem} item2 */
static equal (item1, item2, matchStackSize = true, matchNbt = true, sameStack = false) {
if (item1 == null && item2 == null) {
return true
} else if (item1 == null) {
return false
} else if (item2 == null) {
return false
} else {
return (
item1.type === item2.type &&
(matchStackSize ? item1.count === item2.count : true) &&
item1.metadata === item2.metadata &&
(sameStack ? item1.stackId === item2.stackId : true) &&
(matchNbt
? JSON.stringify(item1.nbt) === JSON.stringify(item2.nbt) &&
item1.canPlaceOnList.sort().toString() === item2.canPlaceOnList.sort().toString() &&
item1.canDestroyList.sort().toString() === item2.canDestroyList.sort().toString()
: true)
)
}
}

// Stack ID
static currentStackId = 0
static nextStackId () {
return BedrockItem.currentStackId++
}

toNetwork (serverAuthoritative = true) {
if (this.type === 0) return { network_id: 0 }

return {
network_id: this.type,
count: this.count,
metadata: this.metadata,
has_stack_id: serverAuthoritative,
stack_id: serverAuthoritative ? this.stackId : undefined,
block_runtime_id: 0, // TODO
extra: {
has_nbt: this.nbt !== null,
nbt: this.nbt !== null ? { version: 1, nbt: this.nbt } : undefined,
can_place_on: this.canPlaceOnList,
can_destroy: this.canDestroyList
}
}
}

/** @returns {BedrockItem} */
static fromNetwork (item, serverAuthoritative = true) {
return new BedrockItem(
item.network_id,
item.count,
item.metadata,
item.extra.nbt?.nbt ?? null,
item.extra.can_place_on,
item.extra.can_destroy,
item.stack_id
)
}

get customName () {
if (Object.keys(this).length === 0) return null
return this.nbt?.value?.display?.value?.Name?.value ?? null
}

set customName (newName) {
if (!this.nbt) this.nbt = { name: '', type: 'compound', value: {} }
if (!this.nbt.value.display) this.nbt.value.display = { type: 'compound', value: {} }
this.nbt.value.display.value.Name = { type: 'string', value: newName }
}

get customLore () {
if (Object.keys(this).length === 0) return null
return this.nbt?.value?.display?.value?.Lore?.value.value ?? null
}

set customLore (newLore) {
if (!this.nbt) this.nbt = { name: '', type: 'compound', value: {} }
if (!this.nbt.value.display) this.nbt.value.display = { type: 'compound', value: {} }
this.nbt.value.display.value.Lore = { type: 'list', value: { type: 'string', value: newLore } }
}

get repairCost () {
if (Object.keys(this).length === 0) return 0
return this.nbt?.value.RepairCost?.value ?? 0
}

set repairCost (value) {
if (!this.nbt) this.nbt = { name: '', type: 'compound', value: {} }
this.nbt.value.RepairCost = { type: 'int', value }
}

get enchants () {
if (Object.keys(this).length === 0) return 0
if (!this.nbt?.value?.ench) return []
return this.nbt.value.ench.value.value.map((ench) => ({
lvl: ench.lvl.value,
name: registry.enchantments[ench.id.value]?.name || null // TODO: bedrock enchantments
}))
}

set enchants (normalizedEnchArray) {
const enchs = normalizedEnchArray.map(({ name, lvl }) => ({
id: { type: 'short', value: registry.enchantmentsByName[name].id },
lvl: { type: 'short', value: lvl }
}))

if (enchs.length !== 0) {
if (!this.nbt) this.nbt = { name: '', type: 'compound', value: {} }
this.nbt.value.ench = { type: 'list', value: { type: 'compound', value: enchs } }
}
}

get durabilityUsed () {
if (Object.keys(this).length === 0) return null
return this.nbt?.value?.Damage?.value ?? 0
}

set durabilityUsed (value) {
if (!this.nbt) this.nbt = { name: '', type: 'compound', value: {} }
this.nbt.value.Damage = { type: 'int', value }
}

get spawnEggMobName () {
return this.name.replace('_spawn_egg', '')
CreeperG16 marked this conversation as resolved.
Show resolved Hide resolved
}

/** @returns {boolean} */
canPlaceOn (block) {
if (typeof block === 'string') {
return this.canPlaceOnList.includes(block) || this.canPlaceOnList.includes(`minecraft:${block}`)
} else {
return this.canPlaceOnList.includes(block.name) || this.canPlaceOnList.includes(`minecraft:${block.name}`)
}
}

/** @returns {boolean} */
canDestroy (block) {
if (typeof block === 'string') {
return this.canDestroyList.includes(block) || this.canDestroyList.includes(`minecraft:${block}`)
} else {
return this.canDestroyList.includes(block.name) || this.canDestroyList.includes(`minecraft:${block.name}`)
}
}
}

return BedrockItem
}

module.exports = loader