@boem312/minecraft-server
Version:
A pure JS library to create Minecraft Java 1.16.3 servers
363 lines (298 loc) • 12.2 kB
JavaScript
const { defaults } = require('../../settings.json')
const { entities, entityAnimations, sounds, soundChannels } = require('../../functions/loader/data.js');
const { applyDefaults } = require('../../functions/applyDefaults.js');
const { entities: clientEntities } = require('./Client/properties/public/dynamic/entities.js');
const { uuid } = require('../../functions/uuid.js');
const path = require('path');
const CustomError = require('./CustomError.js')
const Changeable = require('./Changeable.js');
const Text = require('../exports/Text.js');
const _p = Symbol('private');
const events = Object.freeze([
'leftClick',
'rightClick'
])
const observables = [
'position'
];
const defaultPrivate = {
emitObservable(type) {
this.p.observables[type].forEach(cb => cb(this[type]))
},
emit(event, ...args) {
if (this.p.events[event])
this.p.events[event].forEach(({ callback }) => callback(...args));
this.p.events[event] = this.p.events[event].filter(({ once }) => once === false);
}
};
const changePosition = function (pos, oldValue) {
const { x, y, z, yaw: ya, pitch: pit } = applyDefaults(pos, oldValue);
let yaw = ya % 256;
if (yaw > 127) yaw -= 256;
let pitch = pit % 256;
if (pitch > 127) pitch -= 256;
const xChange = Math.abs(x - oldValue.x);
const yChange = Math.abs(y - oldValue.y);
const zChange = Math.abs(z - oldValue.z);
let usingSmallChangePacket = true;
if (xChange >= 8) usingSmallChangePacket = false;
if (yChange >= 8) usingSmallChangePacket = false;
if (zChange >= 8) usingSmallChangePacket = false;
if (usingSmallChangePacket) {
const locationChanged = x !== oldValue.x || y !== oldValue.y || z !== oldValue.z;
const rotationChanged = yaw !== oldValue.yaw || pitch !== oldValue.pitch;
const dX = ((x * 32) - (oldValue.x * 32)) * 128;
const dY = ((y * 32) - (oldValue.y * 32)) * 128;
const dZ = ((z * 32) - (oldValue.z * 32)) * 128;
if (locationChanged && rotationChanged)
this.p.sendPacket('entity_move_look', {
entityId: this.id,
dX,
dY,
dZ,
yaw,
pitch,
onGround: true //todo
})
else if (locationChanged)
this.p.sendPacket('rel_entity_move', {
entityId: this.id,
dX,
dY,
dZ,
onGround: true //todo
})
else if (rotationChanged)
this.p.sendPacket('entity_look', {
entityId: this.id,
yaw,
pitch,
onGround: true //todo
})
} else
this.p.sendPacket('entity_teleport', {
entityId: this.id,
x,
y,
z,
yaw,
pitch,
onGround: true //todo
});
let changed = [
'x',
'y',
'z',
'yaw',
'pitch'
].some(val => arguments[0][val] !== undefined && oldValue[val] !== arguments[0][val])
if (changed)
this.p.emitObservable.call(this, 'position')
}
class Entity {
constructor(
client,
type,
id,
{
x,
y,
z,
yaw = defaults.entity.position.yaw,
pitch = defaults.entity.position.pitch
},
sendPacket,
extraInfo,
{
sendSpawnPacket = true,
beforeRemove = []
} = {}
) {
this.client = client;
this.sever = client.server;
this.p.beforeRemove = beforeRemove;
let [typeId, e] = getEntity(type) || [];
if (!e)
this.client.p.emitError(new CustomError('expectationNotMet', 'libraryUser', `type in new ${this.constructor.name}(..., ${require('util').inspect(type)}, ..., ...) `, {
got: type,
expectationType: 'type',
expectation: 'entityName',
externalLink: '{docs}/types/entityName'
}, this.constructor, { server: this.client.server, client: this.client }))
this.type = type;
this.living = e.living;
this.id = id;
this.uuid = uuid();
if (!this.client.p.stateHandler.checkReady.call(this.client))
return;
this.p.typeId = typeId;
this.p.observables = Object.fromEntries(observables.map(a => [a, []]));
this.p.sendPacket = sendPacket;
this.p._position = new Changeable((value, oldValue) => changePosition.call(this, value, oldValue), { x, y, z, yaw, pitch }) //todo: add changePosition to private
this.p.events = Object.fromEntries(events.map(a => [a, []]));
if (sendSpawnPacket !== false)
if (this.living)
this.p.sendPacket('spawn_entity_living', {
entityId: this.id,
entityUUID: this.uuid,
type: this.p.typeId,
x: this.position.x,
y: this.position.y,
z: this.position.z,
yaw: this.position.yaw,
pitch: this.position.pitch,
headPitch: 0, //todo
velocityX: 0, //todo
velocityY: 0, //todo
velocityZ: 0 //todo
})
else
this.p.sendPacket('spawn_entity', {
entityId: this.id,
objectUUID: this.uuid,
type: this.p.typeId,
x: this.position.x,
y: this.position.y,
z: this.position.z,
pitch: this.position.pitch,
yaw: this.position.yaw,
objectData: 0, //todo
velocityX: 0, //todo
velocityY: 0, //todo
velocityZ: 0 //todo
})
}
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.');
}
get position() {
return this.p._position;
}
set position(newValue) {
let oldValue = Object.assign({}, this.position.raw)
this.position.setRaw(newValue)
changePosition.call(this, newValue, oldValue)
}
observe(observable, cb) {
if (!this.p.observables[observable])
this.client.p.emitError(new CustomError('expectationNotMet', 'libraryUser', `observable in <${this.constructor.name}>.observe(${require('util').inspect(observable)}, ...) `, {
got: observable,
expectationType: 'value',
expectation: Object.keys(this.p.observables)
}, this.observe, { server: this.client.server, client: this.client }))
this.p.observables[observable].push(cb)
}
animation(animationType) {
if (!this.client.p.stateHandler.checkReady.call(this.client))
return;
if (!entityAnimations.includes(animationType))
this.client.p.emitError(new CustomError('expectationNotMet', 'libraryUser', `animationType in <${this.constructor.name}>.animation(${require('util').inspect(animationType)}) `, {
got: animationType,
expectationType: 'value',
expectation: entityAnimations
}, this.rawListeners, { server: this.client.server, client: this.client }))
this.p.sendPacket('animation', {
entityId: this.id,
animation: entityAnimations.indexOf(animationType)
})
}
camera() {
if (!this.client.p.stateHandler.checkReady.call(this.client))
return;
this.p.sendPacket('camera', {
cameraId: this.id
})
}
sound({ sound, channel, volume, pitch }) {
if (!this.client.p.stateHandler.checkReady.call(this.client))
return;
if (!sounds.find(a => a.name === sound))
this.client.p.emitError(new CustomError('expectationNotMet', 'libraryUser', `sound in <${this.constructor.name}>.sound({ sound: ${require('util').inspect(sound)} }) `, {
got: sound,
expectationType: 'type',
expectation: 'soundName',
externalLink: '{docs}/types/soundName'
}, this.sound, { server: this.client.server, client: this.client }))
if (!soundChannels.includes(channel))
this.client.p.emitError(new CustomError('expectationNotMet', 'libraryUser', `channel in <${this.constructor.name}>.sound({ channel: ${require('util').inspect(channel)} }) `, {
got: channel,
expectationType: 'value',
expectation: soundChannels
}, this.sound, { server: this.client.server, client: this.client }))
this.p.sendPacket('entity_sound_effect', {
soundId: sounds.find(a => a.name === sound).id,
soundCategory: soundChannels.indexOf(channel),
entityId: this.id,
volume,
pitch
})
}
remove() {
for (const func of this.p.beforeRemove)
func();
//todo: check if Client is ready?
this.p.sendPacket('entity_destroy', {
entityIds: [this.id]
});
let newClientEntities = Object.assign({}, this.client.entities);
delete newClientEntities[this.id];
clientEntities.set.call(this.client, Object.freeze(newClientEntities));
}
killClient(deathMessage = '') {
//todo: check if Client is ready?
if (!(deathMessage instanceof Text))
deathMessage = new Text(deathMessage);
this.p.sendPacket('combat_event', {
event: 2,
playerId: this.client.entityId,
entityId: this.id, //killer
message: JSON.stringify(deathMessage.chat)
});
}
removeAllListeners(event) {
if (event)
this.p.events[event] = [];
else
for (const event of Object.keys(this.p.events))
this.p.events[event] = [];
}
on(event, callback) {
if (!events.includes(event))
this.client.p.emitError(new CustomError('expectationNotMet', 'libraryUser', `event in <${this.constructor.name}>.on(${require('util').inspect(event)}, ...) `, {
got: event,
expectationType: 'value',
expectation: events
}, this.on, { server: this.client.server, client: this.client }))
this.p.events[event].push({ callback, once: false });
}
once(event, callback) {
if (!events.includes(event))
this.client.p.emitError(new CustomError('expectationNotMet', 'libraryUser', `event in <${this.constructor.name}>.once(${require('util').inspect(event)}, ...) `, {
got: event,
expectationType: 'value',
expectation: events
}, this.on, { server: this.client.server, client: this.client }))
this.p.events[event].push({ callback, once: true });
}
}
function getEntity(searchName) {
const index = entities.findIndex(({ name }) => name === searchName);
if (index === -1)
return undefined;
return [index, entities[index]];
}
module.exports = Entity