diff --git a/TODO.md b/TODO.md index b2b5b3c..6974ddf 100644 --- a/TODO.md +++ b/TODO.md @@ -20,11 +20,11 @@ This is a list that will keep the developers and the community organized! - [x] Other dimensions - [x] Block dropping - [x] Entity basics + - [x] Plugin System ### To-do #### Priority - [ ] Inventories (saving the blocks players get) - - [ ] Plugin System - [ ] Crafting and Smelting - [ ] Add in cluster functionality diff --git a/doc/api.md b/doc/api.md index fdda9f9..0ed2b98 100644 --- a/doc/api.md +++ b/doc/api.md @@ -19,6 +19,7 @@ - [serv.time](#servtime) - [serv.tickCount](#servtickcount) - [serv.doDaylightCycle](#servdodaylightcycle) + - [serv.plugins](#servplugins) - [Events](#events) - ["error" (error)](#error-error) - ["clientError" (client,error)](#clienterror-clienterror) @@ -48,21 +49,72 @@ - [server._writeAll(packetName, packetFields)](#server_writeallpacketname-packetfields) - [server._writeArray(packetName, packetFields, playerArray)](#server_writearraypacketname-packetfields-playerarray) - [server._writeNearby(packetName, packetFields, loc)](#server_writenearbypacketname-packetfields-loc) - - [Player](#player) + - [Entity](#entity-1) - [Properties](#properties-1) - - [player.entity](#playerentity) + - [entity.id](#entityid) + - [entity.position](#entityposition) + - [entity.world](#entityworld) + - [entity.type](#entitytype) + - [entity.entityType](#entityentitytype) + - [entity.nearbyEntities](#entitynearbyentities) + - [entity.viewDistance](#entityviewdistance) + - [entity.health](#entityhealth) + - [entity.pitch](#entitypitch) + - [entity.headPitch](#entityheadpitch) + - [entity.yaw](#entityyaw) + - [entity.gravity](#entitygravity) + - [entity.terminalvelocity](#entityterminalvelocity) + - [entity.friction](#entityfriction) + - [entity.size](#entitysize) + - [entity.deathTime](#entitydeathtime) + - [entity.pickupTime](#entitypickuptime) + - [entity.bornTime](#entityborntime) + - [entity.itemId](#entityitemid) + - [entity.itemDamage](#entityitemdamage) + - [entity.metadata](#entitymetadata) + - [entity.nearbyEntities](#entitynearbyentities-1) + - [Events](#events-1) + - [Behaviors](#behaviors) + - [FORMAT](#format) + - ["move"](#move) + - [Methods](#methods-1) + - [entity.getData(pluginName)](#entitygetdatapluginname) + - [entity.getOthers()](#entitygetothers) + - [entity.getOtherPlayers()](#entitygetotherplayers) + - [entity.getNearby()](#entitygetnearby) + - [entity.getNearbyPlayers()](#entitygetnearbyplayers) + - [entity.nearbyPlayers()](#entitynearbyplayers) + - [Low level Methods](#low-level-methods) + - [entity._writeOthers(packetName, packetFields)](#entity_writeotherspacketname-packetfields) + - [entity._writeOthersNearby(packetName, packetFields)](#entity_writeothersnearbypacketname-packetfields) + - [Player](#player) + - [Properties](#properties-2) - [player.username](#playerusername) - [player.view](#playerview) - - [player.world](#playerworld) - - [player.nearbyPlayers](#playernearbyplayers) - - [Events](#events-1) + - [Events](#events-2) - ["connected"](#connected) - ["spawned"](#spawned) - ["disconnected"](#disconnected) - ["chat" (message)](#chat-message) - ["kicked" (kicker,reason)](#kicked-kickerreason) - ["positionChanged"](#positionchanged) - - [Methods](#methods-1) + - [Behaviors](#behaviors-1) + - ["move"](#move-1) + - ["look"](#look) + - ["chat"](#chat) + - ["command"](#command) + - ["punch"](#punch) + - ["sendBlock"](#sendblock) + - ["sendChunk"](#sendchunk) + - ["dig"](#dig) + - ["dug"](#dug) + - ["cancelDig"](#canceldig) + - ["forceCancelDig"](#forcecanceldig) + - ["breakAnimation"](#breakanimation) + - ["placeBlock"](#placeblock) + - ["attack"](#attack) + - ["requestRespawn"](#requestrespawn) + - [Methods](#methods-2) - [player.login()](#playerlogin) - [player.ban(reason)](#playerbanreason) - [player.kick(reason)](#playerkickreason) @@ -83,8 +135,6 @@ - [Low level properties](#low-level-properties) - [player._client](#player_client) - [Low level methods](#low-level-methods-1) - - [player._writeOthers(packetName, packetFields)](#player_writeotherspacketname-packetfields) - - [player._writeOthersNearby(packetName, packetFields)](#player_writeothersnearbypacketname-packetfields) @@ -159,6 +209,10 @@ Best to use with modulo (e.g. Something every 10 seconds is `serv.tickCount % 20 Default `true`. If false, time will not automatically pass. +#### serv.plugins + +List of all plugins. Use serv.plugins[pluginName] to get a plugin's object and data. + ### Events #### "error" (error) @@ -293,13 +347,227 @@ Writes packet to every player in playerArray Writes packet to all players within distance of loc. loc has the same paramater as loc in server.getNearby() -## Player +## Entity + +Players are a type of entity, so they will have most of the attributes and methods ### Properties -#### player.entity +#### entity.id -The entity of the player, of type `Flying-squid.Entity` +ID of entity on server + +#### entity.position + +Current position (currently in fixed position (x32 what you'd expect) so do entity.position.scaled(1/32) to get normal position) + +#### entity.world + +World object entity is in + +#### entity.type + +Either "player", "mob", or "object" (currently) + +#### entity.entityType + +Sub-category of entity. For mobs, this is which mob (Zombie/Skeleton, etc). For objects, this is which object (Arrow/Dropped item, etc) + +#### entity.nearbyEntities + +Nearby entities to this entity + +#### entity.viewDistance + +How far away entities are loaded/unloaded (used for players ATM) + +#### entity.health + +How many half-hearts an entity has of health (e.g. Player has 20). Not really used for objects, only players and mobs. + +#### entity.pitch + +Pitch of entity (rotation sideways) + +#### entity.headPitch + +Pitch of entity's head + +#### entity.yaw + +Yaw of entity (rotation looking up and down) + +#### entity.gravity + +Gravity of entity (non-players) to calculate physics. + +#### entity.terminalvelocity + +Only applies to gravity, really. You can still apply a velocity larger than terminal velocity. + +#### entity.friction + +Decreases velocity when touching blocks + +#### entity.size + +Used to calculate collisions for server-side entities + +#### entity.deathTime + +How much time before an entity despawns (in ms) + +#### entity.pickupTime + +How long before an entity can be picked up (in ms) + +#### entity.bornTime + +When an entity was born. Used with pickupTime and deathTime (time in epoch) + +#### entity.itemId + +If a block drop, what item id + +#### entity.itemDamage + +If a block drop, what item damage + +#### entity.metadata + +Metadata for the entity (not like block metadata/damage). Contains stuff like NBT. + +#### entity.nearbyEntities + +List of entities that the entity believes is nearby. + +### Events + + + +### Behaviors + +Behaviors are very interesting. Let me explain to you how they work: + +Behaviors are a special type of event. They are editable and allow defaults to be cancellable making the powerful +for plugins to take control of and interact with each other. Three different events get called +for a behavior: +- EVENTNAME_cancel +- EVENTNAME +- EVENTNAME_done + +EVENTNAME_cancel passses the paramaters `data` (object of all info about behavior. Changing the data could have effects on outcome) and `cancel`, a function. This event is run before the default action. If `cancel()` is called, it will cancel the default action. More on this later. + +EVENTNAME passes `data` as well as `cancelled` so plugins can check if the default behavior has been cancelled. This is event is run +before the default action. + +EVENTNAME_done passes `data` and `cancelled`. This event is run before the default action. + +Example: One plugin wants to cancel a player's movement while another wants to say "HI" when they move + +Plugin A: +```js +player.on('move_cancel', ({position}, cancel) => { + cancel(); // If player tries to move, shoots them back where they came from +}); +``` + +Plugin B: +```js +player.on('move', ({position}, cancelled) => { + if (!cancelled) player.chat('HI!'); +}) +``` + +When a player normally moves, the server saves their position and sends it to all clients. Therefore, if a "move" behavior was truly cancelled, +the player would be able to move freely while the server and other players would see the player stationary. This doesn't happen because +behaviors can have "default cancel functions". In the case of a player's "move", the default cancel function sends them back where they +came from. To prevent this from happening, use the "preventDefaultCancel" paramater: cancel(false); + +Plugin C +```js +player.on('move_cancel', ({position}, cancel) => { + cancel(false); // Doesn't teleport player back +}); +``` + +If we keep Plugin B and replace Plugin A with Plugin C, we'll see that the player can move freely but will not recieve the +word "HI" and other players will be unable to see their movements. + +Finally, there is hidden cancel. This is the second paramater in cancel, and allows plugins to hide the fact that they cancelled +the default action from other plugins. It's best not to use this, but I know somebody will someday need this. + +Plugin D +```js +player.on('move_cancel', ({position}, cancel) => { + cancel(false, true); // Player doesn't teleport back and now "cancelled" will be false +}) +``` + +Using Plugin B and D together, the player will be able to move freely and will be spammed with "HI", however the server will not store +their position and other players will not see the player move. + +#### FORMAT + +Defition of behavior. +- var1: Variable with value, can be changed (default: defaultValue) +- var2 (u): Variable with value. You can change it however it will not have any effect on the defautl action (and could screw with other plugins, watch out!). U stands for unused + +Default: What happens if this isn't cancelled. + +Cancelled: What happens if this is cancelled and preventDefaultCancel is still false. + +#### "move" + +Emitted when server calculates new position for the entity (DOES NOT APPLY TO PLAYER!) +- old (u): Where the entity came from +- onGround (u): If the entity is on the ground + +Default: Send entity relative-move or teleport packets to all nearby players + +Cancelled: Set entity position to old position + +### Methods + +#### entity.getData(pluginName) + +Gets object that stores data, personalized per plugin. Returns null if plugin does not exist. + +Shortcut for: entity.pluginData[pluginName]; + +#### entity.getOthers() + +Get every other entity other than self + +#### entity.getOtherPlayers() + +Gets every player other than self (all players if entity is not a player) + +#### entity.getNearby() + +Gets all entities nearby (within entity.viewDistance) + +#### entity.getNearbyPlayers() + +Gets all nearby players regardless of what client thinks + +#### entity.nearbyPlayers() + +Gets all nearby players that client can see + +### Low level Methods + +#### entity._writeOthers(packetName, packetFields) + +Writes to all other players on server + +#### entity._writeOthersNearby(packetName, packetFields) + +Writes to all players within viewDistance + +## Player + +### Properties #### player.username @@ -309,14 +577,6 @@ The username of the player The view size of the player, for example 8 for 16x16 -#### player.world - -The world which the player is in. - -#### player.nearbyPlayers - -Nearby players. - ### Events #### "connected" @@ -343,6 +603,176 @@ Fires when the player says `message`. fires when the position changes in small amounts (walking, running, or flying) +### Behaviors + +See entity "Behaviors" for more info + +#### "move" + +When player tries to move +- position (u): New position player is trying to move to +- onGround (u): Whether player thinks they're on the ground or not + +Default: Save position/onGround and write to all nearby players + +Cancelled: Snap back to old position + +#### "look" + +When player tries to look somewhere +- yaw (u): New yaw player is looking +- pitch (u): New pitch player is looking +- onGround (u): If player thinks they're on the ground + +Default: Save look directions, send to all nearby players + +Cancelled: Snap their view back to old yaw and pitch + +#### "chat" + +Emitted when player tries to say something (unless they're message starts with /, then refer to "command") +- message (u): Message player sent +- broadcastMessage: What is put in server chat (Default: message) + +Default: Broadcasts to server their message + +Cancelled: Nothing + +#### "command" + +Emitted when player starts their message with a slash +- command: Their commands (excludes the slash) + +Default: Handle command by command system + +Cancelled: Nothing + +#### "punch" + +When player tries to punch nothing + +Default: Send punch animation to nearby players + +Cancelled: Nothing + +#### "sendBlock" + +Emitted when sending a block to a player (block changed). This is separate for every player, cancelling this for one player causes ghost blocks! +- position: Position of the block +- id: ID of the block +- data: Metadata of the block + +Default: Send block change to player. + +Cancelled: Nothing + +#### "sendChunk" + +Emitted when sending a chunk to a player (loading it in) +- x: Chunk X +- z: Chunk Z +- chunk: Chunk data + +Default: Continue sending chunk to client + +Cancelled: Nothing + +#### "dig" + +Emitted when any player STARTS digging (i.e. survival only) +- position: Position of block being mined +- block (u): Block being mined + +Default: Allow player to start mining block, send changes in break animation to other players + +Cancelled: Stop them from digging + +#### "dug" + +Emitted when a player finishes digging something (or a player in creative breaks a block) +- position: Position of block dug +- block (u): Block dug +- dropBlock: Should it drop a block object (Default: false in creative, otherwise true) +- blockDropPosition: Where block is dropped (Default: center of block) +- blockDropWorld: World block is dropped in (Default is the world the player/block is in) +- blockDropVelocity: The velocity the block has when dropped (Default: random) +- blockDropId: ID of the block dropped +- blockDropDamage: Damage of the block dropped +- blockDropPickup: Time before user can pick up the block (Default: 0.5 seconds) +- blockDropDeath: Time before item despawns (Default: 5 minutes) + +Default: Save new block as air, sends to all nearby players + +Cancelled: Send to player the block that was there + +#### "cancelDig" + +Emitted when a player cancels digging in the middle (i.e. survival only) +- position: Position of block that was being mined +- block (u): Block that was being mined + +Default: Stop animation for all players, save stop digging + +Cancelled: Nothing + +#### "forceCancelDig" + +Emitted when the server cancels a dig (currently only happens if the player mines too fast) +- stop: Whether the digging should be cancelled because they mined too fast (Default: true) +- start (u): Time mining started +- time (u): How long the player has been mining + +##### "breakAnimation" + +Emitted when the server believes the break animation should increase (not sent by client!) +- position: Position of block being updated +- state: New state being changed to +- lastState (u): Last state of block +- start (u): When mining started +- timePassed (u): How long between start and now + +Default: Send animation to everyone + +Cancelled: Nothing + +#### "placeBlock" + +Emitted when a player places a block +- position: Position they're attempting to place the block +- id: Id of block being placed +- damage: Data of block being placed +- reference (u): Reference block that was placed on +- direction (u): Direction vector from reference to position +- playSound: Which sound to play (Default: true) +- sound: Sound to play (Default: default sound for that material) + +Default: Place block for server and nearby players + +Cancelled: Replace block with old block for player + +#### "attack" + +Emitted when a player attacks an entity +- attackedEntity: Entity being attacked +- playSound: Play sound (Default: true) +- sound: Sound to play (default is game.player.hurt) +- damage: Damage to deal (default is based off player's weapon, player's potions, attackEntity's potions, and attackedEntity armor) +- velocity: Which way should attackedEntity move when hit +- maxVelocity: maxVelocity from consecutive hits +- animation: Play death/hit animation + +Default: Damage entity, play sound, send velocity, play animation for death/hit + +Cancelled: Nothing + +#### "requestRespawn" + +Emitted when a player tries to respawn + +Default: Let them respawn + +Cancelled: Nothing. You monster. + ### Methods #### player.login() @@ -430,10 +860,4 @@ The internal implementation to communicate with a client ### Low level methods -#### player._writeOthers(packetName, packetFields) - -write to other players than `player` the packet `packetName` with fields `packetFields` - -#### player._writeOthersNearby(packetName, packetFields) - -write to other players in same world that are within 150 blocks (see player.getNearby()) +Same as entity diff --git a/package.json b/package.json index af024c5..2a00b2f 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "babel-runtime": "^5.4.4", + "emit-then": "^1.0.2", "minecraft-data": "0.7.0", "minecraft-protocol": "0.16.2", "mkdirp": "0.5.1", @@ -32,10 +33,10 @@ "prismarine-block": "0.1.0", "prismarine-chunk": "0.2.1", "prismarine-entity": "0.1.0", - "prismarine-world": "0.3.3", - "prismarine-world-sync": "0.1.0", "prismarine-item": "0.0.0", "prismarine-windows": "0.0.0", + "prismarine-world": "0.3.3", + "prismarine-world-sync": "0.1.0", "random-seed": "^0.2.0", "request-promise": "^0.4.3", "requireindex": "~1.0.0", diff --git a/src/index.js b/src/index.js index 8cc72f0..5978229 100644 --- a/src/index.js +++ b/src/index.js @@ -3,6 +3,7 @@ var EventEmitter = require('events').EventEmitter; var path = require('path'); var requireIndex = require('requireindex'); var plugins = requireIndex(path.join(__dirname, 'lib', 'plugins')); +require('emit-then').register(); if (process.env.NODE_ENV === 'dev'){ require('longjohn'); } diff --git a/src/lib/behavior.js b/src/lib/behavior.js index 2ae13b4..e9a2b8e 100644 --- a/src/lib/behavior.js +++ b/src/lib/behavior.js @@ -1,21 +1,25 @@ module.exports = (obj) => { - return async (eventName, data, func, opt) => { + return async (eventName, data, func, cancelFunc) => { var hiddenCancelled = false; var cancelled = false; var cancelCount = 0; - var cancel = (hidden) => { // Hidden shouldn't be used often but it's not hard to implement so meh + var defaultCancel = true; + var cancel = (dC=true, hidden=false) => { // Hidden shouldn't be used often but it's not hard to implement so meh if (hidden) hiddenCancelled = true; else { cancelled = true; cancelCount++; } + defaultCancel = dC; } - await obj.emit(eventName + '_cancel', data, cancel); - await obj.emit(eventName, data, cancelled, cancelCount); + await obj.emitThen(eventName + '_cancel', data, cancel).catch((err)=> setTimeout(() => {throw err;},0)); + await obj.emitThen(eventName, data, cancelled, cancelCount).catch((err)=> setTimeout(() => {throw err;},0)); - if (!hiddenCancelled && !cancelled) await func(data); + if (!hiddenCancelled && !cancelled) await func(data).catch((err)=> setTimeout(() => {throw err;},0)); + else if (cancelFunc && defaultCancel) await cancelFunc(data).catch((err)=> setTimeout(() => {throw err;},0)); - await obj.emit(eventName + '_done', data, cancelled); + await obj.emitThen(eventName + '_done', data, cancelled).catch((err)=> setTimeout(() => {throw err;},0)); + return data; } } \ No newline at end of file diff --git a/src/lib/plugins/animations.js b/src/lib/plugins/animations.js index 19f3a3f..db6818b 100644 --- a/src/lib/plugins/animations.js +++ b/src/lib/plugins/animations.js @@ -1,10 +1,14 @@ module.exports.player=function(player) { player._client.on("arm_animation", () => - player._writeOthersNearby("animation", { - entityId: player.id, - animation: 0 - })); + player.behavior('punch', {}, () => { + player._writeOthersNearby("animation", { + entityId: player.id, + animation: 0 + }); + }) + ); + player._client.on("entity_action", ({actionId} = {}) => { if(actionId == 3) { diff --git a/src/lib/plugins/blocks.js b/src/lib/plugins/blocks.js index 909673c..efd5528 100644 --- a/src/lib/plugins/blocks.js +++ b/src/lib/plugins/blocks.js @@ -13,10 +13,17 @@ module.exports.player=function(player,serv) }; player.sendBlock = (position, blockType, blockData) => // Call from player.setBlock unless you want "local" fake blocks - player._client.write("block_change",{ + player.behavior('sendBlock', { + position: position, + id: blockType, + data: blockData + }, ({position, id, damage}) => { + player._client.write("block_change",{ location:position, type:blockType<<4 | blockData + }); }); + player.setBlock = (position,blockType,blockData) => serv.setBlock(player.world,position,blockType,blockData); diff --git a/src/lib/plugins/chat.js b/src/lib/plugins/chat.js index d5ea1c2..c2f3c9d 100644 --- a/src/lib/plugins/chat.js +++ b/src/lib/plugins/chat.js @@ -11,12 +11,19 @@ module.exports.player=function(player,serv) { player._client.on('chat', ({message} = {}) => { if(message[0]=="/") { - var command = message.slice(1); - player.handleCommand(command); + player.behavior('command', { + command: message.slice(1) + }, ({command}) => { + player.handleCommand(command); + }); } else { - serv.broadcast('<' + player.username + '>' + ' ' + message); - player.emit("chat",message); + player.behavior('chat', { + message: message, + broadcastMessage: '<' + player.username + '>' + ' ' + message + }, ({message, broadcast, broadcastMessage}) => { + serv.broadcast(broadcastMessage); + }); } }); diff --git a/src/lib/plugins/commands.js b/src/lib/plugins/commands.js index c0f26cb..009a88e 100644 --- a/src/lib/plugins/commands.js +++ b/src/lib/plugins/commands.js @@ -75,6 +75,6 @@ module.exports.player=function(player) { player.handleCommand = (str) => { - player.commands.use(str); + player.commands.use(str).catch((err)=> setTimeout(() => {throw err;},0)); }; }; diff --git a/src/lib/plugins/daycycle.js b/src/lib/plugins/daycycle.js index 6048f86..6c9ffd5 100644 --- a/src/lib/plugins/daycycle.js +++ b/src/lib/plugins/daycycle.js @@ -14,7 +14,12 @@ module.exports.server=function(serv) { serv.on('tick', (delta,count) => { if (!serv.doDaylightCycle) return; if (count % 20 == 0) { - serv.setTime((serv.time + 20) % 24000); // Vanilla only does it every second + serv.behavior('changeTime', { + old: serv.time, + newTime: serv.time + 20 + }, ({newTime}) => { + serv.setTime((serv.time + 20) % 24000); // Vanilla only does it every second + }); } }) }; diff --git a/src/lib/plugins/digging.js b/src/lib/plugins/digging.js index 378e28e..d9f7a62 100644 --- a/src/lib/plugins/digging.js +++ b/src/lib/plugins/digging.js @@ -2,48 +2,36 @@ var Vec3 = require("vec3").Vec3; module.exports.player=function(player,serv) { + + function cancelDig({position, block}) { + player.sendBlock(position, block.type, block.metadata); + } + player._client.on("block_dig",({location,status} = {}) => { var pos=new Vec3(location.x,location.y,location.z); player.world.getBlock(pos) .then(block => { - player.behavior('digPacket', { - position: pos, - status: status, - block: block - }, ({position,status,block}) => { - currentlyDugBlock=block; - if(currentlyDugBlock.type==0) return; - if(status==0 && player.gameMode!=1) - player.behavior('dig', { // Start dig survival - position: position, - block: block - }, ({position}) => { - return startDigging(position); - }); - else if(status==2) - player.behavior('finishDig', { // Finish dig survival - position: position, - block: block - }, ({position}) => { - return completeDigging(position); - }); - else if(status==1) - player.behavior('cancelDig', { // Cancel dig survival - position: position, - block: block - }, ({position}) => { - return cancelDigging(position); - }); - else if(status==0 && player.gameMode==1) - player.behavior('dig', { // Start/finish dig creative - position: position, - block: block - }, ({position}) => { - return creativeDigging(position); - }); - } - )} - ) + currentlyDugBlock=block; + if(currentlyDugBlock.type==0) return; + if(status==0 && player.gameMode!=1) + player.behavior('dig', { // Start dig survival + position: pos, + block: block + }, ({position}) => { + return startDigging(position); + }, cancelDig); + else if(status==2) + completeDigging(pos); + else if(status==1) + player.behavior('cancelDig', { // Cancel dig survival + position: pos, + block: block + }, ({position}) => { + return cancelDigging(position); + }); + else if(status==0 && player.gameMode==1) + return creativeDigging(pos); + }) .catch((err)=> setTimeout(() => {throw err;},0)) }); @@ -75,12 +63,20 @@ module.exports.player=function(player,serv) newDestroyState=newDestroyState>9 ? 9 : newDestroyState; if(newDestroyState!=lastDestroyState) { - lastDestroyState=newDestroyState; - player._writeOthersNearby("block_break_animation",{ - "entityId":currentAnimationId, - "location":location, - "destroyStage":newDestroyState - }); + player.behavior('breakAnimation', { + lastState: lastDestroyState, + state: newDestroyState, + start: startDigging, + timePassed: currentDiggingTime, + position: location + }, ({lastState, state}) => { + lastDestroyState=state; + player._writeOthersNearby("block_break_animation",{ + "entityId":currentAnimationId, + "location":location, + "destroyStage":state + }); + }) } } } @@ -99,14 +95,32 @@ module.exports.player=function(player,serv) { clearInterval(animationInterval); var diggingTime=new Date()-startDiggingTime; + var stop = false; if(expectedDiggingTime-diggingTime<100) { - player.changeBlock(location,0,0); - // Drop block - serv.spawnObject(2, player.world, location.offset(0.5, 0.5, 0.5), { - velocity: new Vec3(Math.random()*4 - 2, Math.random()*2 + 2, Math.random()*4 - 2), - itemId: currentlyDugBlock.type, - itemDamage: currentlyDugBlock.metadata - }); + stop = true; + stop = player.behavior('forceCancelDig', { + stop: true, + start: startDiggingTime, + time: diggingTime + }).stop; + } + if(!stop) { + player.behavior('dug', { + position: location, + block: currentlyDugBlock, + dropBlock: true, + blockDropPosition: location.offset(0.5, 0.5, 0.5), + blockDropWorld: player.world, + blockDropVelocity: new Vec3(Math.random()*4 - 2, Math.random()*2 + 2, Math.random()*4 - 2), + blockDropId: currentlyDugBlock.type, + blockDropDamage: currentlyDugBlock.metadata, + blockDropPickup: 500, + blockDropDeath: 60*5*1000 + }, (data) => { + player.changeBlock(data.position,0,0); + console.log('dropping',data.dropBlock); + if (data.dropBlock) dropBlock(data); + }, cancelDig) } else { @@ -117,10 +131,34 @@ module.exports.player=function(player,serv) } } + function dropBlock({blockDropPosition, blockDropWorld, blockDropVelocity, blockDropId, blockDropDamage, blockDropPickup, blockDropDeath}) { + serv.spawnObject(2, blockDropWorld, blockDropPosition, { + velocity: blockDropVelocity, + itemId: blockDropId, + itemDamage: blockDropDamage, + pickupTime: blockDropPickup, + deathTime: blockDropDeath + }); + } + function creativeDigging(location) { - return player.changeBlock(location,0,0); + player.behavior('dug', { + position: location, + block: currentlyDugBlock, + dropBlock: false, + blockDropPosition: location.offset(0.5, 0.5, 0.5), + blockDropWorld: player.world, + blockDropVelocity: new Vec3(Math.random()*4 - 2, Math.random()*2 + 2, Math.random()*4 - 2), + blockDropId: currentlyDugBlock.type, + blockDropDamage: currentlyDugBlock.metadata, + blockDropPickup: 500, + blockDropDeath: 60*5*1000 + }, (data) => { + player.changeBlock(data.position,0,0); + if (data.dropBlock) dropBlock(data); + }, cancelDig); } }; \ No newline at end of file diff --git a/src/lib/plugins/entities.js b/src/lib/plugins/entities.js index 3771561..4fa85e3 100644 --- a/src/lib/plugins/entities.js +++ b/src/lib/plugins/entities.js @@ -25,7 +25,7 @@ module.exports.server=function(serv,options) { return entity; }; - serv.spawnObject = (type, world, position, {pitch=0,yaw=0,velocity=new Vec3(0,0,0),data=1,itemId,itemDamage=0}={}) => { + serv.spawnObject = (type, world, position, {pitch=0,yaw=0,velocity=new Vec3(0,0,0),data=1,itemId,itemDamage=0,pickupTime=500,deathTime=60*1000}={}) => { var object = serv.initEntity('object', type, world, position.scaled(32).floored()); object.data = data; object.velocity = velocity.scaled(32).floored(); @@ -35,8 +35,8 @@ module.exports.server=function(serv,options) { object.terminalvelocity = new Vec3(27*32, 27*32, 27*32); object.friction = new Vec3(15*32, 0, 15*32); object.size = new Vec3(0.25*32, 0.25*32, 0.25*32); // Hardcoded, will be dependent on type! - object.deathTime = 60*1000; // 60 seconds - object.pickupTime = 200; + object.deathTime = deathTime; + object.pickupTime = pickupTime; object.itemId = itemId; object.itemDamage = itemDamage; @@ -167,7 +167,7 @@ module.exports.entity=function(entity,serv){ }; - entity.on("positionChanged",() => { + entity.on("move",() => { if(entity.position.distanceTo(entity.lastPositionPlayersUpdated)>2*32) entity.updateAndSpawn(); }); @@ -259,7 +259,8 @@ module.exports.entity=function(entity,serv){ entity.collect = (collectEntity) => { if (entity.type != 'player'){ - serv.emit('error', 'Non-player entity (ttype ' + entity.type + ') cannot collect another entity') + console.log('[ERROR] Non-player entity (type ' + entity.type + ') cannot collect another entity'); + console.log((new Error()).stack); return; } diff --git a/src/lib/plugins/external.js b/src/lib/plugins/external.js index 09faa89..f711957 100644 --- a/src/lib/plugins/external.js +++ b/src/lib/plugins/external.js @@ -1,3 +1,5 @@ +var fs = require('fs'); + module.exports.server = function(serv, settings) { serv.plugins = {}; serv.pluginCount = 0; @@ -19,15 +21,19 @@ module.exports.server = function(serv, settings) { } for (var p in settings.plugins) { + if (settings.plugins[p].disabled) continue; try { - serv.addPlugin(p, require(p), settings.plugins[p]); + require.resolve(p); // Check if it exists, if not do catch, otherwise jump to bottom } catch (err) { - try { - serv.addPlugin(p, require('../../plugins/' + p), settings.plugins[p]); + try { // Throw error if cannot find plugin + fs.accessSync('./dist/plugins/' + p); } catch (err) { throw new Error('Cannot find plugin "' + p + '"'); } + serv.addPlugin(p, require('../../plugins/' + p), settings.plugins[p]); + return; } + serv.addPlugin(p, require(p), settings.plugins[p]); } for (var p in serv.plugins) { diff --git a/src/lib/plugins/log.js b/src/lib/plugins/log.js index 711c280..735e773 100644 --- a/src/lib/plugins/log.js +++ b/src/lib/plugins/log.js @@ -53,7 +53,7 @@ module.exports.player=function(player,serv) player.on("disconnected",() => serv.log("[INFO]: " + player.username + ' disconnected')); - player.on("chat", message => serv.log("[INFO] " + '<' + player.username + '>' + ' ' + message)); + player.on("chat", ({message}) => serv.log("[INFO] " + '<' + player.username + '>' + ' ' + message)); player.on("kicked",(kicker,reason) => serv.log(kicker.username + " kicked " + player.username + (reason ? " (" + reason + ")" : ""))); diff --git a/src/lib/plugins/placeBlock.js b/src/lib/plugins/placeBlock.js index ab7e6df..daf0265 100644 --- a/src/lib/plugins/placeBlock.js +++ b/src/lib/plugins/placeBlock.js @@ -26,15 +26,15 @@ module.exports.player=function(player,serv) position: placedPosition, reference: referencePosition, playSound: true, - sound: 'dig.' + (materialToSound[blocks[heldItem.blockId].material] || 'stone') - }, ({direction, heldItem, position, reference, playSound, sound}) => { + sound: 'dig.' + (materialToSound[blocks[heldItem.blockId].material] || 'stone'), + }, ({direction, heldItem, position, reference, playSound, sound, id, damage}) => { if (playSound) { serv.playSound(sound, player.world, placedPosition.clone().add(new Vec3(0.5, 0.5, 0.5)), { pitch: 0.8 }); } if(heldItem.blockId!=323){ - player.changeBlock(position,heldItem.blockId,heldItem.itemDamage); + player.changeBlock(position, id, damage); }else if(direction==1){ player.setBlock(position, 63, 0); player._client.write('open_sign_entity', { @@ -46,6 +46,10 @@ module.exports.player=function(player,serv) location:position }); } + }, async () => { + var id = await player.world.getBlockType(placedPosition); + var damage = await player.world.getBlockData(placedPosition); + player.sendBlock(placedPosition, id, damage); }); }); }; diff --git a/src/lib/plugins/pvp.js b/src/lib/plugins/pvp.js index 18262e8..78c5414 100644 --- a/src/lib/plugins/pvp.js +++ b/src/lib/plugins/pvp.js @@ -17,22 +17,31 @@ module.exports.player=function(player,serv) var attackedEntity = serv.entities[entityId]; if(!attackedEntity || (attackedEntity.gameMode != 0 && attackedEntity.type == 'player')) return; - attackedEntity.updateHealth(attackedEntity.health - 1); - serv.playSound('game.player.hurt', player.world, attackedEntity.position.scaled(1/32)); + player.behavior('attack', { + attackedEntity: attackedEntity, + sound: 'game.player.hurt', + playSound: true, + damage: 1, + velocity: attackedEntity.position.minus(player.position).plus(new Vec3(0, 0.5, 0)).scaled(5), + maxVelocity: new Vec3(4, 4, 4), + animation: true + }, ({attackedEntity, sound, playSound, damage, velocity, maxVelocity, animation}) => { + attackedEntity.updateHealth(attackedEntity.health - damage); + serv.playSound(sound, player.world, attackedEntity.position.scaled(1/32)); - var attackVelocity = attackedEntity.position.minus(player.position).plus(new Vec3(0, 0.5, 0)).scaled(5/32); - attackedEntity.sendVelocity(attackVelocity, new Vec3(4, 4, 4)); + attackedEntity.sendVelocity(velocity.scaled(1/32), maxVelocity); - if(attackedEntity.health<=0) - attackedEntity._writeOthers('entity_status',{ + if(attackedEntity.health<=0 && animation) + attackedEntity._writeOthers('entity_status',{ + entityId:attackedEntity.id, + entityStatus:3 + }); + else if (animation) + attackedEntity._writeOthers('animation',{ entityId:attackedEntity.id, - entityStatus:3 + animation:1 }); - else - attackedEntity._writeOthers('animation',{ - entityId:attackedEntity.id, - animation:1 - }); + }); } player._client.on("use_entity", ({mouse,target} = {}) => { diff --git a/src/lib/plugins/respawn.js b/src/lib/plugins/respawn.js index bacc817..a1bdc89 100644 --- a/src/lib/plugins/respawn.js +++ b/src/lib/plugins/respawn.js @@ -2,16 +2,18 @@ module.exports.player=function(player) { player._client.on("client_command", ({payload}) => { if(payload == 0) { - player._client.write("respawn",{ - dimension:0, - difficulty:0, - gamemode:player.gameMode, - levelType:'default' + player.behavior('requestRespawn', {}, () => { + player._client.write("respawn",{ + dimension:0, + difficulty:0, + gamemode:player.gameMode, + levelType:'default' + }); + player.sendPosition(); + player.updateHealth(20); + player.nearbyEntities=[]; + player.updateAndSpawn(); }); - player.sendPosition(); - player.updateHealth(20); - player.nearbyEntities=[]; - player.updateAndSpawn(); } }); }; \ No newline at end of file diff --git a/src/lib/plugins/sound.js b/src/lib/plugins/sound.js index fd4f281..1c91f20 100644 --- a/src/lib/plugins/sound.js +++ b/src/lib/plugins/sound.js @@ -40,30 +40,27 @@ module.exports.player=function(player,serv) { serv.playSound(sound, player.world, null, opt); }; - player._client.on('block_place', ({location}={}) => { + player.on('placeBlock_cancel', async ({position, reference}, cancel) => { if (player.crouching) return; - var pos=new Vec3(location.x,location.y,location.z); - player.world.getBlockType(pos).then((id) => { - if (id != 25) return; - if (!player.world.blockEntityData[pos.toString()]) player.world.blockEntityData[pos.toString()] = {}; - var data = player.world.blockEntityData[pos.toString()]; - if (typeof data.note == 'undefined') data.note = -1; - data.note++; - data.note %= 25; - serv.playNoteBlock(data.note, player.world, pos); - }).catch((err)=> setTimeout(() => {throw err;},0)); + var id = await player.world.getBlockType(reference); + if (id != 25) return; + cancel(false); + if (!player.world.blockEntityData[reference.toString()]) player.world.blockEntityData[reference.toString()] = {}; + var data = player.world.blockEntityData[reference.toString()]; + if (typeof data.note == 'undefined') data.note = -1; + data.note++; + data.note %= 25; + serv.playNoteBlock(data.note, player.world, reference); }); - player._client.on('block_dig', ({location,status} = {}) => { - if (status != 0 || player.gameMode == 1) return; - var pos=new Vec3(location.x,location.y,location.z); - player.world.getBlockType(pos).then((id) => { - if (id != 25) return; - if (!player.world.blockEntityData[pos.toString()]) player.world.blockEntityData[pos.toString()] = {}; - var data = player.world.blockEntityData[pos.toString()]; - if (typeof data.note == 'undefined') data.note = 0; - serv.playNoteBlock(data.not,player.world, pos, data.note); - }).catch((err)=> setTimeout(() => {throw err;},0)); + player.on('dig_cancel', async ({position}, cancel) => { + var id = await player.world.getBlockType(position); + if (id != 25) return; + cancel(false); + if (!player.world.blockEntityData[position.toString()]) player.world.blockEntityData[position.toString()] = {}; + var data = player.world.blockEntityData[position.toString()]; + if (typeof data.note == 'undefined') data.note = 0; + serv.playNoteBlock(data.note ,player.world, position); }); diff --git a/src/lib/plugins/updatePositions.js b/src/lib/plugins/updatePositions.js index 749f41d..6f8f823 100644 --- a/src/lib/plugins/updatePositions.js +++ b/src/lib/plugins/updatePositions.js @@ -17,21 +17,29 @@ module.exports.player=function(player) } function sendLook(yaw,pitch,onGround) { - var convYaw=conv(yaw); - var convPitch=conv(pitch); - if (convYaw == player.yaw && convPitch == player.pitch) return; - player._writeOthersNearby("entity_look", { - entityId: player.id, - yaw: convYaw, - pitch: convPitch, + player.behavior('look', { + yaw: yaw, + pitch: pitch, onGround: onGround - }); - player.yaw = convYaw; - player.pitch = convPitch; - player.onGround = onGround; - player._writeOthersNearby("entity_head_rotation", { - entityId: player.id, - headYaw: convYaw + }, () => { + var convYaw=conv(yaw); + var convPitch=conv(pitch); + if (convYaw == player.yaw && convPitch == player.pitch) return; + player._writeOthersNearby("entity_look", { + entityId: player.id, + yaw: convYaw, + pitch: convPitch, + onGround: onGround + }); + player.yaw = convYaw; + player.pitch = convPitch; + player.onGround = onGround; + player._writeOthersNearby("entity_head_rotation", { + entityId: player.id, + headYaw: convYaw + }); + }, () => { + player.sendPosition(); }); } @@ -44,33 +52,39 @@ module.exports.player=function(player) }); function sendRelativePositionChange(newPosition, onGround) { - if (player.position.distanceTo(new Vec3(0, 0, 0)) != 0) { - var diff = newPosition.minus(player.position); - if(diff.abs().x>127 || diff.abs().y>127 || diff.abs().z>127) - { - player._writeOthersNearby('entity_teleport', { - entityId:player.id, - x: newPosition.x, - y: newPosition.y, - z: newPosition.z, - yaw: player.yaw, - pitch: player.pitch, - onGround: onGround - }); + player.behavior('move', { + onGround: onGround, + position: newPosition + }, () => { + if (player.position.distanceTo(new Vec3(0, 0, 0)) != 0) { + var diff = newPosition.minus(player.position); + if(diff.abs().x>127 || diff.abs().y>127 || diff.abs().z>127) + { + player._writeOthersNearby('entity_teleport', { + entityId:player.id, + x: newPosition.x, + y: newPosition.y, + z: newPosition.z, + yaw: player.yaw, + pitch: player.pitch, + onGround: onGround + }); + } + else if (diff.distanceTo(new Vec3(0, 0, 0)) != 0) { + player._writeOthersNearby('rel_entity_move', { + entityId: player.id, + dX: diff.x, + dY: diff.y, + dZ: diff.z, + onGround: onGround + }); + } } - else if (diff.distanceTo(new Vec3(0, 0, 0)) != 0) { - player._writeOthersNearby('rel_entity_move', { - entityId: player.id, - dX: diff.x, - dY: diff.y, - dZ: diff.z, - onGround: onGround - }); - } - } - player.position = newPosition; - player.onGround = onGround; - player.emit("positionChanged"); + player.position = newPosition; + player.onGround = onGround; + }, () => { + player.sendPosition(); + }); } player.sendPosition = () => { @@ -87,26 +101,31 @@ module.exports.player=function(player) module.exports.entity=function(entity,serv){ entity.sendPosition = ({oldPos,onGround}) => { - var diff = entity.position.minus(oldPos); - - if(diff.abs().x>127 || diff.abs().y>127 || diff.abs().z>127) - entity._writeOthersNearby('entity_teleport', { - entityId: entity.id, - x: entity.position.x, - y: entity.position.y, - z: entity.position.z, - yaw: entity.yaw, - pitch: entity.pitch, - onGround: onGround - }); - else if (diff.distanceTo(new Vec3(0, 0, 0)) != 0) serv._writeNearby('rel_entity_move', { - entityId: entity.id, - dX: diff.x, - dY: diff.y, - dZ: diff.z, + entity.behavior('move', { + old: oldPos, onGround: onGround - }, entity); + }, ({old,onGround}) => { + var diff = entity.position.minus(oldPos); - entity.emit('positionChanged', oldPos); + if(diff.abs().x>127 || diff.abs().y>127 || diff.abs().z>127) + entity._writeOthersNearby('entity_teleport', { + entityId: entity.id, + x: entity.position.x, + y: entity.position.y, + z: entity.position.z, + yaw: entity.yaw, + pitch: entity.pitch, + onGround: onGround + }); + else if (diff.distanceTo(new Vec3(0, 0, 0)) != 0) serv._writeNearby('rel_entity_move', { + entityId: entity.id, + dX: diff.x, + dY: diff.y, + dZ: diff.z, + onGround: onGround + }, entity); + }, () => { + entity.position = oldPos; + }); }; }; \ No newline at end of file diff --git a/src/lib/plugins/world.js b/src/lib/plugins/world.js index 9fab63d..1804d58 100644 --- a/src/lib/plugins/world.js +++ b/src/lib/plugins/world.js @@ -70,14 +70,20 @@ module.exports.player=function(player,serv) { player.sendChunk = (chunkX,chunkZ,column) => { - player._client.write('map_chunk', { + return player.behavior('sendChunk', { x: chunkX, z: chunkZ, - groundUp: true, - bitMap: 0xffff, - chunkData: column.dump() - }); - return Promise.resolve(); + chunk: column + }, ({x, z, chunk}) => { + player._client.write('map_chunk', { + x: x, + z: z, + groundUp: true, + bitMap: 0xffff, + chunkData: chunk.dump() + }); + return Promise.resolve(); + }) }; function spiral(arr)