flying-squid
Version:
A minecraft server written in node.js
460 lines (445 loc) • 12.1 kB
JavaScript
/* global BigInt */
const fs = require('fs')
const Vec3 = require('vec3').Vec3
const nbt = require('prismarine-nbt')
const long = require('long')
const { gzip } = require('node-gzip')
const { promisify } = require('util')
const convertInventorySlotId = require('./convertInventorySlotId')
const nbtParse = promisify(nbt.parse)
const playerDefaults = {
health: 20,
food: 20,
heldItemSlot: 0
}
module.exports = { read, save, playerDefaults }
async function read (uuid, spawnPoint, worldFolder) {
try {
const playerDataFile = await fs.promises.readFile(`${worldFolder}/playerdata/${uuid}.dat`)
const playerData = (await nbtParse(playerDataFile)).value
return {
player: {
health: playerData.Health.value,
food: playerData.foodLevel.value,
gameMode: playerData.playerGameType.value,
xp: playerData.XpTotal.value,
heldItemSlot: playerData.SelectedItemSlot.value,
position: new Vec3(playerData.Pos.value.value[0], playerData.Pos.value.value[1], playerData.Pos.value.value[2]),
yaw: playerData.Rotation.value.value[0],
pitch: playerData.Rotation.value.value[1],
onGround: Boolean(playerData.OnGround.value)
},
inventory: playerData.Inventory.value.value.map(nbtItem => {
if (nbtItem?.tag && !nbtItem.tag.name) nbtItem.tag.name = ''
return nbtItem
})
}
} catch (e) {
return {
player: { ...playerDefaults, ...{ position: spawnPoint.clone() } },
inventory: []
}
}
}
async function save (player, worldFolder, snakeCase, theFlattening) {
if (worldFolder === undefined) {
return
}
function playerInventoryToNBT (playerInventory) {
const nbtInventory = []
playerInventory.slots.forEach(item => {
if (item) {
const nbtItem = {
Slot: {
type: 'byte',
value: convertInventorySlotId.toNBT(item.slot)
},
id: {
type: 'string',
value: `minecraft:${item.name}`
},
Count: {
type: 'byte',
value: item.count
}
}
if (!theFlattening) {
Object.assign(nbtItem, {
Damage: {
type: 'short',
value: item.metadata
}
})
} else if (item.nbt) {
Object.assign(nbtItem, {
tag: item.nbt
})
}
nbtInventory.push(nbtItem)
}
})
return nbtInventory
}
try {
const playerDataFile = await fs.promises.readFile(`${worldFolder}/playerdata/${player.uuid}.dat`)
const newUncompressedData = await nbtParse(playerDataFile)
newUncompressedData.value.Health.value = player.health
newUncompressedData.value.foodLevel.value = player.food
newUncompressedData.value.playerGameType.value = player.gameMode
newUncompressedData.value.XpTotal.value = player.xp
newUncompressedData.value.SelectedItemSlot.value = player.heldItemSlot
newUncompressedData.value.Pos.value.value = [player.position.x, player.position.y, player.position.z]
newUncompressedData.value.Rotation.value.value = [player.yaw, player.pitch]
newUncompressedData.value.OnGround.value = Number(player.onGround)
newUncompressedData.value.Inventory.value.value = playerInventoryToNBT(player.inventory)
const newDataCompressed = await gzip(nbt.writeUncompressed(newUncompressedData))
await fs.promises.writeFile(`${worldFolder}/playerdata/${player.uuid}.dat`, newDataCompressed)
} catch (e) {
// Get UUIDMost & UUIDLeast. mc-uuid-converter Copyright (c) 2020 Sol Toder https://github.com/AjaxGb/mc-uuid-converter under MIT License.
const uuidBytes = new Uint8Array(16)
const uuid = new DataView(uuidBytes.buffer)
const hexText = player.uuid.includes('-')
? player.uuid.trim()
.split('-')
.map((g, i) => g.padStart([8, 4, 4, 4, 12][i], '0'))
.join('')
: player.uuid.trim().padStart(32, '0')
uuid.setBigUint64(0, BigInt('0x' + hexText.substring(0, 16)), false)
uuid.setBigUint64(8, BigInt('0x' + hexText.substring(16)), false)
const UUIDMostLong = long.fromString(uuid.getBigInt64(0, false).toString(), false)
const UUIDLeastLong = long.fromString(uuid.getBigInt64(8, false).toString(), false)
const newUncompressedData = {
type: 'compound',
name: '',
value: {
HurtByTimestamp: {
type: 'int',
value: 0
},
SleepTimer: {
type: 'short',
value: 0
},
Attributes: {
type: 'list',
value: {
type: 'compound',
value: [
{
Base: {
type: 'double',
value: 20
},
Name: {
type: 'string',
value: snakeCase ? 'generic.max_health' : 'generic.maxHealth'
}
},
{
Base: {
type: 'double',
value: 0
},
Name: {
type: 'string',
value: snakeCase ? 'generic.knockback_resistance' : 'generic.knockbackResistance'
}
},
{
Base: {
type: 'double',
value: 0.10000000149011612
},
Name: {
type: 'string',
value: snakeCase ? 'generic.movement_speed' : 'generic.movementSpeed'
}
},
{
Base: {
type: 'double',
value: 0
},
Name: {
type: 'string',
value: 'generic.armor'
}
},
{
Base: {
type: 'double',
value: 0
},
Name: {
type: 'string',
value: snakeCase ? 'generic.armor_toughness' : 'generic.armorToughness'
}
},
{
Base: {
type: 'double',
value: 1
},
Name: {
type: 'string',
value: snakeCase ? 'generic.attack_damage' : 'generic.attackDamage'
}
},
{
Base: {
type: 'double',
value: 4
},
Name: {
type: 'string',
value: snakeCase ? 'generic.attack_speed' : 'generic.attackSpeed'
}
},
{
Base: {
type: 'double',
value: 0
},
Name: {
type: 'string',
value: 'generic.luck'
}
}
]
}
},
Invulnerable: {
type: 'byte',
value: 0
},
FallFlying: {
type: 'byte',
value: 0
},
PortalCooldown: {
type: 'int',
value: 0
},
AbsorptionAmount: {
type: 'float',
value: 0
},
abilities: {
type: 'compound',
value: {
invulnerable: {
type: 'byte',
value: 0
},
mayfly: {
type: 'byte',
value: 0
},
instabuild: {
type: 'byte',
value: 0
},
walkSpeed: {
type: 'float',
value: 0.10000000149011612
},
mayBuild: {
type: 'byte',
value: 1
},
flying: {
type: 'byte',
value: 0
},
flySpeed: {
type: 'float',
value: 0.05000000074505806
}
}
},
FallDistance: {
type: 'float',
value: 0
},
recipeBook: {
type: 'compound',
value: {
recipes: {
type: 'list',
value: {
type: 'end',
value: []
}
},
isFilteringCraftable: {
type: 'byte',
value: 0
},
toBeDisplayed: {
type: 'list',
value: {
type: 'end',
value: []
}
},
isGuiOpen: {
type: 'byte',
value: 0
}
}
},
DeathTime: {
type: 'short',
value: 0
},
XpSeed: {
type: 'int',
value: 0
},
XpTotal: {
type: 'int',
value: player.xp
},
playerGameType: {
type: 'int',
value: player.gameMode
},
seenCredits: {
type: 'byte',
value: 0
},
Motion: {
type: 'list',
value: {
type: 'double',
value: [
0,
-0.0784000015258789,
0
]
}
},
UUIDLeast: {
type: 'long',
value: [
UUIDLeastLong.high,
UUIDLeastLong.low
]
},
Health: {
type: 'float',
value: player.health
},
foodSaturationLevel: {
type: 'float',
value: 5
},
Air: {
type: 'short',
value: 300
},
OnGround: {
type: 'byte',
value: Number(player.onGround)
},
Dimension: {
type: 'int',
value: 0
},
Rotation: {
type: 'list',
value: {
type: 'float',
value: [
player.yaw,
player.pitch
]
}
},
XpLevel: {
type: 'int',
value: 0
},
Score: {
type: 'int',
value: 0
},
UUIDMost: {
type: 'long',
value: [
UUIDMostLong.high,
UUIDMostLong.low
]
},
Sleeping: {
type: 'byte',
value: 0
},
Pos: {
type: 'list',
value: {
type: 'double',
value: [
player.position.x,
player.position.y,
player.position.z
]
}
},
Fire: {
type: 'short',
value: -20
},
XpP: {
type: 'float',
value: 0
},
EnderItems: {
type: 'list',
value: {
type: 'end',
value: []
}
},
DataVersion: {
type: 'int',
value: 1343
},
foodLevel: {
type: 'int',
value: player.food
},
foodExhaustionLevel: {
type: 'float',
value: 0
},
HurtTime: {
type: 'short',
value: 0
},
SelectedItemSlot: {
type: 'int',
value: player.heldItemSlot
},
Inventory: {
type: 'list',
value: {
type: 'compound',
value: playerInventoryToNBT(player.inventory)
}
},
foodTickTimer: {
type: 'int',
value: 0
}
}
}
const newDataCompressed = await gzip(nbt.writeUncompressed(newUncompressedData))
try {
await fs.promises.mkdir(`${worldFolder}/playerdata/`, { recursive: true })
} catch (err) {
// todo fix browserfs behavior instead
}
await fs.promises.writeFile(`${worldFolder}/playerdata/${player.uuid}.dat`, newDataCompressed)
}
}