Merge pull request #49 from demipixel/plugins

The start of plugins with World Guard example
This commit is contained in:
mhsjlw 2015-09-27 17:56:47 -04:00
commit b2d189b9d2
18 changed files with 575 additions and 8 deletions

2
.gitignore vendored
View file

@ -1 +1,3 @@
node_modules
plugins/*
!plugins/README.md

View file

@ -39,8 +39,17 @@
- ["spawned"](#spawned)
- ["disconnected"](#disconnected)
- ["error" (error)](#error-error-1)
- ["chat" (message)](#chat-message)
- ["kicked" (kicker,reason)](#kicked-kickerreason)
- [Cancelable Behaviors](#cancelable-behaviors)
- ["chatMessage"](#chatmessage)
- ["chat"](#chat)
- ["command"](#command)
- ["startDig"](#startdig)
- ["stopDig"](#stopdig)
- ["finishDig"](#finishdig)
- ["placeBlock"](#placeblock)
- ["attackPlayer"](#attackplayer)
- ["animation_arm"](#animation_arm)
- [Methods](#methods-1)
- [player.login()](#playerlogin)
- [player.ban(reason)](#playerbanreason)
@ -202,14 +211,96 @@ Fires when the player disconnected
Fires when there is an error.
#### "chat" (message)
Fires when the player says `message`.
#### "kicked" (kicker,reason)
`kicker` kick the player with `reason`
### Cancelable Behaviors
This type of event is emitted by the the player with the option to cancel a default. It is primarily used by external plugins.
This type of event is emitted twice. For example, if a player digs a block, both digBlock\_cancel and digBlock are emitted.
digBlock\_cancel has the ability to cancel the default action. digBlock allows plugins to check if the default has been cancelled before it runs. An example with finishDig:
```js
player.on("finishDig_cancel", function(event, cancel) {
if (event.block.id == 1) { // If player mined stone (id == 1)
cancel(); // Do not break the block in the world, do not send block change to others
}
});
```
```js
player.on("finishDig", function(event, cancelled) {
if (!cancelled) { // Make sure another plugin has not cancelled the default response
if (event.block.id == 1) player.chat("You broke stone!");
}
});
```
For these, the cancel event is always originalName_cancel with arguments (event, cancel)
The "check cancel" event is always originalName with arguments (event, cancelled)
#### "chatMessage"
Fires when a user sends any message to the server (even a command)
- message: String sent by player
#### "chat"
Fires when a user sends a message that does not start with a `/` (i.e. not a command).
- message: String sent by the player
#### "command"
Fires when a user starts a message with a `/`.
- message: String sent by player but without the `/`
#### "startDig"
Fires when a player begins to break a blog (even in creative)
- position: Position block is being mined in the world
- block: Block at that position in world
#### "stopDig"
Fires when a player choses to stop breaking a block
- position: Position block is being mined in the world
- block: Block at that position in world
#### "finishDig"
Fires when a player has finished mining a block. If the player is in creative, this will be called immediately after `startDig`.
- time: Time it took to mine block (0 if player is in creative)
- position: Position block is being mined in the world
- block: Block at that position in world
#### "placeBlock"
Fires when a user places a block
- reference: Position that the player right-clicked on to place the block
- position: Position the user wishes to place the block
- id: Id of the block they are placing
`position` and `id` will soon be replaced by `block` which will contain a Block object.
#### "attackPlayer"
Fires when one player attacks another
- attacked: Player who was attacked
#### "animation_arm"
Fires when a player wants to "punch" (including anything they're holding).
### Methods
#### player.login()

View file

@ -42,6 +42,31 @@ in log.js of playerPlugins or serverPlugins.
## Creating external plugins
When you're making an external plugin, create a repo and publish to NPM your code so others can use it.
However, if you simply want to fool around, create a folder, use `npm init`, and drag it into the "plugins" folder.
Your file's base should look like this:
```js
module.exports = inject;
function inject(serv, player, self, opt) {
}
```
- serv is the Server object. Use this to broadcast messages, set blocks, etc
- player is a Player object. You can make changes to the player or check for events from them.
- self is your plugin. You may need your plugin id, so you'll use `self.id`.
- opt is any options the server has while running.
Since the plugin is its own node module, you can install any other modules inside of it!
Checks the API.md for information about what events you can check for on the server or player!
## Creating external plugins OLD
Create a new repo, which will be published to npm when ready to be used.
Create a file in which you put an inject function like this :

View file

@ -0,0 +1,13 @@
module.exports=inject;
function inject(serv, player, self) {
serv.broadcast('Hey ' + player.username + '!');
player.setGameMode(1);
player.on('block_place_cancel', function(e, cancel) { // Users can't place any wood planks!
if (e.id == '5') {
cancel();
player.sendBlock(e.position, 0);
}
});
}

View file

@ -0,0 +1,11 @@
{
"name": "flying-squid-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "DemiPixel <luke5227@gmail.com>",
"license": "ISC"
}

View file

@ -0,0 +1,158 @@
module.exports = inject;
function inject(serv, player, self) {
player.plugins[self.id].pos = {
1: null,
2: null
};
player.plugins[self.id].op = false;
self.areas = [];
player = player;
player.on('command_cancel', function(e, cancel) {
var player = this;
var text = e.message;
var split = text.split(' ');
if (split[0] != 'wg') return;
else cancel();
var opped = player.plugins[self.id].op;
if (split[1] == 'pos1' && opped) {
self.setPosition(player, 1, player.entity.position.x>>5, player.entity.position.z>>5);
player.chat('Set pos1 at ' + (player.entity.position.x>>5) + ',' + (player.entity.position.z>>5));
}
else if (split[1] == 'pos2' && opped) {
self.setPosition(player, 2, player.entity.position.x>>5, player.entity.position.z>>5);
player.chat('Set pos2 at ' + (player.entity.position.x>>5) + ',' + (player.entity.position.z>>5));
}
else if (split[1] == 'set' && opped) {
var success = self.setArea(player.plugins[self.id].pos);
if (!success) {
player.chat('You need to set two positions to set an area. Move to the location you want and use:');
player.chat('"/wg pos1" or "/wg pos2"');
} else {
player.chat('Successfully set area! (Size: ' + success.width + 'x' + success.length + ')');
self.clearPosition(player, 1);
self.clearPosition(player, 2);
}
} else if (split[1] == 'clear' && opped) {
self.clearPosition(player, 1);
self.clearPosition(player, 2);
player.chat('Cleared positions');
} else if (split[1] == 'op') {
var playerTarget = serv.getPlayer(split[2]);
if (!playerTarget) {
player.chat('No such player "' + split[2] + '"');
} else {
playerTarget.plugins[self.id].op = true;
player.chat('WG Opped ' + split[2]);
}
} else if (split[1] == 'deop' && opped) {
var playerTarget = serv.getPlayer(split[2]);
if (!playerTarget) {
player.chat('No such player "' + split[2] + '"');
} else {
playerTarget.plugins[self.id].op = false;
player.chat('WG Deopped ' + split[2]);
}
} else if (split[1] == 'help') {
var messages = [
'World Guard is used to restrict building areas!',
'Use /wg pos1 or /wg pos2 to set your two positions.',
'Use /wg set to confirm the positions.',
'Use /wg op <user> to allow a user to set positions or build/mine in restricted areas',
'Use /wg deop <user> to remove op.',
'Use /wg clear to clear positions',
'Use /wg list to list all restricted areas you are in (and their IDs)',
'Use /wg delete <id> to delete a restricted area'
];
for (var m in messages) {
player.chat(messages[m]);
}
} else if (split[1] == 'list' && opped) {
var areas = self.areasInPosition(player.entity.position.x>>5, player.entity.position.z>>5);
if (!areas.length) {
player.chat('You are not in any restricted areas!');
} else player.chat('==== LIST OF AREAS ====');
for (var a in areas) {
var ar = areas[a];
var str = ar[0] + '] ' + ar[1][1].x + ',' + ar[1][1].z + ' to ' + ar[1][2].x + ',' + ar[1][2].z;
str += ' (' + (Math.abs(ar[1][1].x - ar[1][2].x)+1) + 'x' + (Math.abs(ar[1][1].z - ar[1][2].z)+1) + ')';
player.chat(str);
}
} else if (split[1] == 'delete' && opped) {
if (!self.areas[split[2]]) {
player.chat('Area with id ' + split[2] + ' does not exist!');
} else {
self.deleteArea(split[2]);
player.chat('Deleted area (id ' + split[2] + ')');
}
} else {
player.chat('Not a valid World Guard command! Use /wg help');
}
});
player.on('placeBlock_cancel', function(e, cancel) {
if (player.plugins[self.id].op === true) return;
if (self.areasInPosition(e.position.x, e.position.z).length) {
cancel();
player.sendBlock(e.position, 0);
}
});
player.on('finishDig_cancel', function(e, cancel) {
if (player.plugins[self.id].op === true) return;
if (self.areasInPosition(e.position.x, e.position.z).length) {
cancel();
player.sendBlock(e.position, e.block.type);
}
});
self.setPosition = function(player, which, x, z) {
if (which != 1 && which != 2) return;
player.plugins[self.id].pos[which] = { x: x, z: z}
};
self.clearPosition = function(player, which) {
if (which != 1 && which != 2) return;
player.plugins[self.id].pos[which] = null;
}
self.setArea = function(pos) {
if (!pos[1] || !pos[2]) return false;
self.areas.push({
1: {
x: pos[1].x,
z: pos[1].z
},
2: {
x: pos[2].x,
z: pos[2].z
}
});
return {
width: Math.abs(pos[1].x-pos[2].x) + 1,
length: Math.abs(pos[1].z-pos[2].z) + 1
}
}
self.deleteArea = function(id) {
self.areas[id] = null;
}
self.areasInPosition = function(x, z) {
var inside = [];
for (var a in self.areas) {
if (!self.areas[a]) continue;
var x1 = self.areas[a][1].x, x2 = self.areas[a][2].x, z1 = self.areas[a][1].z, z2 = self.areas[a][2].z;
if (x >= Math.min(x1,x2) && x <= Math.max(x1,x2) && z >= Math.min(z1,z2) && z <= Math.max(z1, z2)) {
inside.push([a, self.areas[a]]);
}
}
return inside;
}
}

View file

@ -0,0 +1,11 @@
{
"name": "world-guard",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "DemiPixel <luke5227@gmail.com>",
"license": "ISC"
}

21
lib/cancelEvent.js Normal file
View file

@ -0,0 +1,21 @@
module.exports = emit;
function emit(target, eventName, args, defaultFunc) {
var hiddenCancelled = false;
var cancelled = false;
var cancel = function(hidden) { // Hidden shouldn't be used often but it's not hard to implement so meh
if (hidden) hiddenCancelled = true;
else cancelled = true;
}
target.emit(eventName + '_cancel', args, cancel);
target.emit(eventName, args, cancelled);
if (!hiddenCancelled && !cancelled) {
if (defaultFunc) defaultFunc();
return true;
} else {
return false
}
}

View file

@ -1,8 +1,13 @@
var cancelEmit = require("../cancelEvent");
module.exports=inject;
function inject(serv, player)
{
player._client.on("arm_animation", function(packet) {
var doDefault = cancelEmit(player, "animation_arm", {});
if (!doDefault) return;
player._writeOthers("animation", {
entityId: player.entity.id,
animation: 0

View file

@ -1,15 +1,31 @@
var cancelEmit = require("../cancelEvent");
module.exports=inject;
function inject(serv, player)
{
player._client.on('chat', function (packet) {
var doDefault = cancelEmit(player, "chatMessage", {
message: packet.message
});
if (!doDefault) return;
if(packet.message[0]=="/") {
var doDefault = cancelEmit(player, "command", {
message: packet.message.slice(1)
});
if (!doDefault) return;
var command = packet.message.slice(1);
player.handleCommand(command);
}
else {
var doDefault = cancelEmit(player, "chat", {
message: packet.message
});
if (!doDefault) return;
serv.broadcast('<' + player.username + '>' + ' ' + packet.message);
player.emit("chat",packet.message);
}
});

View file

@ -87,6 +87,10 @@ function inject(serv, player, options) {
});
}
}
else if (results = command.match(/^reload/)) {
serv.reloadPlugins();
player.chat('Reloaded ' + serv.plugins.length + ' plugins.');
}
else
player.chat("Invalid command.");
}

View file

@ -1,4 +1,5 @@
var Vec3 = require("vec3");
var cancelEmit = require("../cancelEvent");
module.exports=inject;
@ -6,6 +7,13 @@ function inject(serv,player)
{
player._client.on("block_dig",function(packet){
var pos=new Vec3(packet.location);
var doDefault = cancelEmit(player, "startDig", {
position: pos,
block: serv.world.getBlock(pos)
});
if (!doDefault) return;
currentlyDugBlock=serv.world.getBlock(pos);
if(currentlyDugBlock.type==0) return;
if(packet.status==0 && player.gameMode!=1)
@ -46,6 +54,13 @@ function inject(serv,player)
newDestroyState=newDestroyState>9 ? 9 : newDestroyState;
if(newDestroyState!=lastDestroyState)
{
var doDefault = cancelEmit(player, "breakAnimation", {
lastState: lastDestroyState,
position: location,
block: currentlyDugBlock
});
if (!doDefault) return;
lastDestroyState=newDestroyState;
player._writeOthers("block_break_animation",{
"entityId":currentAnimationId,
@ -59,6 +74,13 @@ function inject(serv,player)
function cancelDigging(location)
{
clearInterval(animationInterval);
var doDefault = cancelEmit(player, "stopDig", {
position: pos,
block: serv.world.getBlock(pos)
});
if (!doDefault) return;
player._writeOthers("block_break_animation",{
"entityId":currentAnimationId,
"location":location,
@ -70,8 +92,15 @@ function inject(serv,player)
{
clearInterval(animationInterval);
var diggingTime=new Date()-startDiggingTime;
if(expectedDiggingTime-diggingTime<100)
if(expectedDiggingTime-diggingTime<100) {
var doDefault = cancelEmit(player, "finishDig", {
time: diggingTime,
position: pos,
block: serv.world.getBlock(pos)
});
if (!doDefault) return;
player.changeBlock(location,0);
}
else
{
player._client.write("block_change",{
@ -84,6 +113,12 @@ function inject(serv,player)
function creativeDigging(location)
{
var doDefault = cancelEmit(player, "finishDig", {
time: 0,
position: location,
block: serv.world.getBlock(location)
});
if (!doDefault) return;
player.changeBlock(location,0);
}

View file

@ -1,4 +1,5 @@
var vec3 = require("vec3");
var cancelEmit = require("../cancelEvent");
module.exports=inject;
@ -9,6 +10,14 @@ function inject(serv,player)
var referencePosition=new vec3(packet.location.x,packet.location.y,packet.location.z);
var directionVector=directionToVector[packet.direction];
var placedPosition=referencePosition.plus(directionVector);
var doDefault = cancelEmit(player, "placeBlock", { // TODO, make block object and send it (instead of ID)
reference: referencePosition,
position: placedPosition,
id: packet.heldItem.blockId
});
if (!doDefault) return;
player.changeBlock(placedPosition,packet.heldItem.blockId);
});
}

View file

@ -1,3 +1,5 @@
var cancelEmit = require("../cancelEvent");
module.exports=inject;
function inject(serv, player)
@ -15,6 +17,12 @@ function inject(serv, player)
function attackEntity(entityId)
{
var attackedPlayer = serv.entities[entityId].player;
var doDefault = cancelEmit(player, "attackPlayer", {
attacked: attackedPlayer
});
if (!doDefault) return;
if(attackedPlayer.gameMode!=0) return;
attackedPlayer.updateHealth(attackedPlayer.entity.health - 1);
@ -32,7 +40,7 @@ function inject(serv, player)
player._client.on("use_entity", function(packet) {
if(packet.mouse == 1) {
attackEntity(packet.target);
if (packet.target.player) attackEntity(packet.target);
}
});

View file

@ -15,5 +15,12 @@ function inject(serv,options)
}
serv.emit("newPlayer",player);
player.login();
player.plugins = Array();
for(var pluginName in serv.plugins) { // External plugins
player.plugins[serv.plugins[pluginName].id] = {}; // Give object to save data per plugin per player, referenced by plugin ID
var plug = require(serv.plugins[pluginName].path)(serv, player, serv.plugins[pluginName], options);
}
});
}

View file

@ -0,0 +1,125 @@
module.exports = inject;
var fs = require('fs');
function inject(serv) {
serv.loadPlugins = loadPlugins;
serv.loadPlugins();
function resetPlayers() {
for (var p in serv.players) {
var player = serv.players[p]
player.plugins = Array();
for (var pl in serv.plugins) {
var plugin = serv.plugins[pl];
player.plugins[plugin.id] = {};
require(plugin.path)(serv, player, plugin);
}
console.log(serv.players[p].plugins);
}
}
serv.reloadPlugins = function() {
console.log('RELOADING DOES NOT WORK');
return;
serv.emit('pluginend');
resetPlayers();
}
serv.fullReloadPlugins = function(cb) {
console.log('RELOADING DOES NOT WORK');
return;
serv.emit('pluginend');
serv.loadPlugins(function() {
resetPlayers();
cb();
});
}
serv.getPlugin = function(name) {
return serv.plugins[name] || null;
}
}
function loadPlugins(cb) {
var serv = this;
serv.plugins = Array();
loadCount = 0;
allPlugins = null;
getNodeModules(serv, setPluginsFromModules, cb);
var pluginPath = __dirname.match(/(.*?)\/lib/)[1] + '/plugins'; // Prob a cleaner way to do this
fs.readdir(pluginPath, function(err, arr) {
if (!arr) setPlugins([], serv);
else {
var plugins = Array();
for (var a in arr) {
if (arr[a].indexOf('.') == 0 || arr[a] == 'README.md') continue;
plugins.push({
name: arr[a],
path: pluginPath + '/' + arr[a]
});
}
setPlugins(plugins, serv, cb);
}
});
}
function setPluginsFromModules(err, modules, serv, cb) {
if (err) {
console.log('ERROR: Error loading node_modules; Cannot load external plugins! /lib/serverPlugins/plugins.js');
serv.emit('error',err);
return;
}
var plugins = Array();
for (var m in modules) {
if (m.indexOf('flying-squid-') == 0) {
var pluginName = m.replace('flying-squid-','');
plugins.push({
name: pluginName,
path: pluginName
});
}
}
setPlugins(plugins, serv, cb);
}
var loadCount = 0;
var allPlugins;
function setPlugins(plugins, serv, cb) {
loadCount++;
if (loadCount < 2) { // Wait for both plugins folder and node_modules to load
allPlugins = plugins;
return;
} else {
plugins = plugins.concat(allPlugins).sort(); // Sorting makes it easy to check duplicates
}
var id = 0;
for (var p in plugins) {
serv.plugins[plugins[p].name] = { // Other info about plugin here, TODO: Add events (i.e. ".on"), allow cancels?
id: id,
path: plugins[p].path,
name: plugins[p].name
};
console.log('Loaded plugin: ' + plugins[p].name);
id++;
if (p < plugins.length-1 && plugins[p].name == plugins[parseInt(p)+1].name) { // Only checks for two duplicates, TODO: check for 3+ duplicates
p++;
}
}
console.log('Loaded ' + id + ' Plugin' + (id != 1 ? 's' : '') );
if (cb) cb();
}
function getNodeModules(serv, cb, cb2) {
require('child_process').exec('npm ls --json', function(err, stdout, stderr) {
if (err) return cb(err, null, serv, cb2);
cb(null, JSON.parse(stdout).dependencies, serv, cb2);
});
}

View file

@ -34,6 +34,7 @@
"prismarine-chunk": "git://github.com/rom1504/prismarine-chunk.git#use-prismarine-block",
"prismarine-entity": "0.1.0",
"prismarine-world": "git://github.com/rom1504/prismarine-world.git#implementation",
"random-seed": "^0.2.0",
"request": "^2.61.0",
"requireindex": "~1.0.0",
"vec3": "0.1.3"

25
plugins/README.md Normal file
View file

@ -0,0 +1,25 @@
## Do you really need to use this folder??
If you know of a plugin on npm or git, simply use in console
```
npm install --save flying-squid-plugin-name
```
Or for a git repository:
```
npm install --save git+https://git@github.com/yourname/repo.git
```
## Using /plugins
Simply create a folder inside of /plugins with the name of your plugin. Inside, do `npm init` and create your index.js!
You need this because npm complains about modules inside of node_modules that are not inside package.json.
**USE THIS SPARINGLY!**
## Contributors
.gitignore ignores everything in this folder except for README.md. Don't worry about removing contents in order to push!