@boem312/minecraft-server
Version:
A pure JS library to create Minecraft Java 1.16.3 servers
219 lines (185 loc) • 7.55 kB
JavaScript
const { applyDefaults } = require('../../functions/applyDefaults');
const { uuid } = require('../../functions/uuid');
const { getSkinTextures } = require('../../functions/getSkinTextures');
const { gamemodes } = require('../../functions/loader/data.js');
const { tabItems } = require('./Client/properties/public/dynamic/tabItems.js');
const settings = require('../../settings.json');
const tabItemDefaults = settings.defaults.tabItem;
const skinFetchTimeout = settings.timing.skinFetchTimeout;
const Text = require('../exports/Text.js');
const path = require('path');
const _p = Symbol('private');
const defaultPrivate = {
parseProperty(key, value) {
if (key === 'name' && !(value instanceof Text))
return new Text(value)
else return value;
},
parseProperties(properties) {
for (const [key, value] of Object.entries(properties))
properties[key] = this.p.parseProperty.call(this, key, value);
return properties;
},
updateProperty(name, oldValue) {
if (!this.client.p.stateHandler.checkReady.call(this.client))
return;
if (name === 'ping')
this.p.sendPacket('player_info', {
action: 2,
data: [{
UUID: this.uuid,
ping: this.ping === null ? -1 : this.ping
}]
})
else if (name === 'name') {
//todo: use <Text> onChange event
this.p.sendPacket('player_info', {
action: 3,
data: [{
UUID: this.uuid,
displayName: JSON.stringify(this.name.chat)
}]
})
} else if (name === 'uuid') {
this.p.textures = null;
if (this.player) {
this.player.p2._.uuid = this.uuid;
this.player.p2.respawn.call(this.player);
} else
this.p.respawn.call(this, oldValue);
}
else if (name === 'skinAccountUuid') {
this.p.textures = null;
if (this.player) {
this.player.p2._.skinAccountUuid = this.skinAccountUuid;
this.player.p2.respawn.call(this.player);
} else
this.p.respawn.call(this, oldValue);
}
},
async spawn(textures) {
this.p.sendPacket('player_info', {
action: 0,
data: [{
UUID: this.uuid,
name: this.p.name, // this the name used if a Player is spawned
displayName: JSON.stringify(this.name.chat),
properties: (textures || await getSkinTextures(this.skinAccountUuid)).properties,
gamemode: gamemodes.indexOf(this.p.gamemode),
ping: this.ping === null ? -1 : this.ping
}]
});
},
remove(oldUuid) {
this.p.sendPacket('player_info', {
action: 4,
data: [{
UUID: oldUuid ?? this.uuid
}]
});
},
async respawn(oldUuid) {
//todo: why can't we get the skinTextures after the TabItem is removed?
const textures = await getSkinTextures(this.skinAccountUuid); //load textures before removing tabItem
this.p.remove.call(this, oldUuid);
await this.p.spawn.call(this, textures);
}
};
const writablePropertyNames = Object.freeze([
'ping',
'name',
'uuid',
'skinAccountUuid'
]);
class TabItem {
constructor(p, client, sendPacket, cb, { sendSpawnPacket = true } = {}) {
this.client = client;
this.server = client.server;
this.p.sendPacket = sendPacket;
let properties = typeof p === 'object' ? Object.assign({}, p) : p;
properties = applyDefaults(properties, tabItemDefaults);
if (properties.uuid === null) { //todo: maybe move to parseProperties
properties.uuid = uuid().split('');
properties.uuid[14] = '2'; // set uuid to version 2 so that it can't be a valid client uuid
properties.uuid = properties.uuid.join('');
if (!properties.skinAccountUuid) properties.skinAccountUuid = null;
} else if (!properties.skinAccountUuid)
properties.skinAccountUuid = properties.uuid;
this.p.gamemode = settings.defaults.gamemode;
//todo: use properties.player
// parseProperties
properties = this.p.parseProperties.call(this, properties);
// set private properties
this.p._ = {};
for (const propertyName of writablePropertyNames)
this.p._[propertyName] = properties[propertyName];
// define getters and setters
for (const propertyName of writablePropertyNames)
Object.defineProperty(this, propertyName, {
configurable: false,
enumerable: true,
get: () => this.p._[propertyName],
set: newValue => {
let oldValue = this.p._[propertyName];
this.p._[propertyName] = this.p.parseProperty.call(this, propertyName, newValue);
this.p.updateProperty.call(this, propertyName, oldValue);
}
});
//todo: implement sorting TabItems (ie setIndex functions), because MC Client sorts tabItems based on name
this.p.name = '';
if (!this.client.p.stateHandler.checkReady.call(this.client))
return;
if (sendSpawnPacket)
this
.p.spawn.call(this)
.then(() => {
tabItems.set.call(this.client, Object.freeze(sortTabItems([...this.client.tabItems, this])));
cb(this);
})
.catch(e => { throw e });
else {
tabItems.set.call(this.client, Object.freeze(sortTabItems([...this.client.tabItems, this])));
cb(this);
}
}
remove() {
if (!this.client.p.stateHandler.checkReady.call(this.client))
return;
if (this.player) {
this.player.tabItem = null;
this.player = null;
}
this.p.remove.call(this);
tabItems.set.call(this.client,
Object.freeze(
sortTabItems(
this.client.tabItems
.filter(tabItem => tabItem !== this)
)
)
);
}
get p() {
let callPath = new Error().stack.split('\n')[2];
if (callPath.includes('('))
callPath = callPath.split('(')[1].split(')')[0];
else
callPath = callPath.split('at ')[1];
callPath = callPath.split(':').slice(0, 2).join(':');
let folderPath = path.resolve(__dirname, '../../');
if (!callPath.startsWith(folderPath))
console.warn('(minecraft-server) WARNING: Detected access to private properties from outside of the module. This is not recommended and may cause unexpected behavior.');
if (!this[_p]) //todo: create private when instantiating class
this[_p] = Object.assign({}, defaultPrivate);
return this[_p];
}
set p(value) {
console.error('(minecraft-server) ERROR: Setting private properties is not supported. Action ignored.');
}
}
function sortTabItems(t) {
let tabItems = Object.assign([], t);
tabItems.sort((a, b) => a.p.name === b.p.name ? 0 : a.p.name < b.p.name ? -1 : 1);
return tabItems;
}
module.exports = TabItem;