Merge pull request #154 from demipixel/inCompSciRightNow

wats ur fav type of icecream
This commit is contained in:
Romain Beaumont 2015-12-11 00:23:08 +01:00
commit b679dd6325
14 changed files with 190 additions and 205 deletions

View file

@ -5,93 +5,67 @@
Directory architecture :
* app.js: specific settings and actually start the server
* index.js: contain the generic server implementation
* lib/: contain the classes and functions used in the plugins
* serverPlugins/: server plugins that do things general to the server,
properties and method are added to the server object in them
* playerPlugins/: player plugins that do things for each player, properties and method are added to the player object in them
* dist/: Contains "compiled" code (go to current directory in console and type `gulp` to generate this)
* src/: Source files for the project
* src/index.js: contain the generic server implementation
* src/lib: contain the classes and functions used in the plugins
* plugins/: All of the default plugins made to simulate vanilla
* worldGeneraions/: Contains default world generations, however plugins can use their own
Structure of a server plugin:
## Structure of a plugin
```js
module.exports=inject;
function inject(serv)
{
// add methods and properties to serv
// Each of these are called an "inject" because you're injecting properties, events, methods, or data into the objects
module.exports.server = function(serv) { // Create your server events here
serv.spawnPoint = ...;
serv.on('...', ...);
}
```
Structure of a player plugin :
```js
module.exports=inject;
function inject(serv,player)
{
// add methods and properties to player
// you can use serv, but you shouldn't add things to it here
module.exports.entity = function(entity, serv) { // Called whenever an entity is created, do NOT do serv.on here
entity.health = 10; // Start with 10 health out of 20
entity.on('...', ...);
// serv.on('...', ...); NOOOO
}
module.exports.player = function(player, serv) { // Player is a type of entity (entity inject is called first) with added properties and functions
player.setXp(100); // Example of a property player entities have but not other entities
player.on(',,,', ...);
// serv.on('...', . don't even think about it
}
```
## Logs and event
In order to keep logging independent from the rest of the server and to let people react in other ways than logging,
server and player events should be emitting and the logging should only take place in response to these events
in log.js of playerPlugins or serverPlugins.
logging uses methods and events from `log.js`. These include `serv.log(message)` and `serv.emit('error', err)`.
## Creating external plugins
Create a new repo, which will be published to npm when ready to be used.
Create a new repo, which will be published to npm when ready to be used. Create a file (probably `index.js`) in which you use a similar format as above (module.exports.xxxx).
Create a file in which you put an inject function like this :
In these inject functions you can use everything documented in the [api.md](api.md).
```js
module.exports=init;
Let's say you called your module fs-flying-horses and you published it to npm.
function init(flying-squid) {
return inject;
}
Now people can use install your plugin by simply typing:
function inject(serv)
{
// add methods and properties to serv
}
```
```npm install fs-flying-horses```
In the init function, you can use anything flying-squid provide
(see [index.js](https://github.com/mhsjlw/flying-squid/blob/master/index.js#L11)).
### Testing your Plugin
In the inject function you can use everything documented in the [api.md](api.md) to add functionalities to the serv object.
For your convenience, you can put your plugin inside /src/plugins. An example might look like:
- src/plugins/
- myPluginName/
- index.js
- package.json
- node_modules
- ...
- myPluginName2.js (direct files are allowed but are impossible to publish, so it's best only to use them for testing)
Let's say you called your module flying-horses and you published it to npm.
Now people can use your plugin that way :
```js
var flyingSquid = require('flying-squid');
var flyingHorses = require('flying-horses')(flyingSquid);
var serv = flyingSquid.createMCServer(/* your options there */);
// install the plugin
flyingHorses(serv);
```
As explained in the first part of this file, flying-squid has 2 kinds of plugins: server plugins, and player plugins.
We've explained until now how to create a server plugin and to use it with flying-squid.
Within the same module, you can also create a player plugin. Here is the code you need to add to do that:
```js
serv.on("newPlayer",function(player){
injectPlayer(serv,player);
});
function injectPlayer(serv,player)
{
// add methods and properties to player
// you can use serv, but you shouldn't add things to it here
}
```
## Conclusion
In this document, we explained how to create a simple plugin with just one file, but you can cut your code
in several files by having several inject function and putting them in different files, just like flying-squid does for its internal plugins.

View file

@ -13,7 +13,8 @@ module.exports = {
Command:require("./lib/command"),
version:require("./lib/version"),
generations:require("./lib/generations"),
experience:require("./lib/experience")
experience:require("./lib/experience"),
UserError:require("./lib/UserError")
};
function createMCServer(options) {
@ -39,5 +40,7 @@ class MCServer extends EventEmitter {
this._server.on('error', error => this.emit('error',error));
this._server.on('listening', () => this.emit('listening',this._server.socketServer.address().port));
this.emit('asap');
process.on('unhandledRejection', err => this.emit('error',err));
}
}

5
src/lib/UserError.js Normal file
View file

@ -0,0 +1,5 @@
class UserError extends Error {
}
module.exports = UserError;

View file

@ -13,13 +13,22 @@ module.exports = (obj) => {
defaultCancel = dC;
};
var resp;
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).catch((err)=> setTimeout(() => {throw err;},0));
else if (cancelFunc && defaultCancel) await cancelFunc(data).catch((err)=> setTimeout(() => {throw err;},0));
if (!hiddenCancelled && !cancelled) {
resp = func(data);
if (resp instanceof Promise) resp = await resp.catch((err)=> setTimeout(() => {throw err;},0));
if (typeof resp == 'undefined') resp = true;
} else if (cancelFunc && defaultCancel) {
resp = cancelFunc(data);
if (resp instanceof Promise) resp = await resp.catch((err)=> setTimeout(() => {throw err;},0));
if (typeof resp == 'undefined') resp = false;
}
await obj.emitThen(eventName + '_done', data, cancelled).catch((err)=> setTimeout(() => {throw err;},0));
return data;
return resp;
}
};

View file

@ -3,6 +3,9 @@ class Command {
this.params = params;
this.parent = parent;
this.hash = parent ? parent.hash : {};
this.uniqueHash = parent ? parent.uniqueHash : {};
this.parentBase = (this.parent && this.parent.base && this.parent.base + ' ') || '';
this.base = this.parentBase + (this.params.base || '');
this.updateHistory();
}
@ -44,9 +47,9 @@ class Command {
updateHistory() {
var all = '(.+?)';
var list = [this.params.base];
var list = [this.base];
if(this.params.aliases && this.params.aliases.length) {
this.params.aliases.forEach(al => list.unshift(al));
this.params.aliases.forEach(al => list.unshift(this.parentBase + al));
}
list.forEach((command) => {
@ -56,6 +59,7 @@ class Command {
if(this.path) this.hash[this.path] = this;
});
this.uniqueHash[this.base] = this;
}
add(params) {

View file

@ -30,24 +30,18 @@ module.exports.player=function(player,serv)
player.commands.add({
base: 'setblock',
info: 'to put a block',
usage: '/setblock <x> <y> <z> <id> <data>',
info: 'set a block at a position',
usage: '/setblock <x> <y> <z> <id> [data]',
op: true,
parse(str) {
var results = str.match(/^(~|~?-?[0-9]*) (~|~?-?[0-9]*) (~|~?-?[0-9]*) ([0-9]{1,3}) ([0-9]{1,3})/);
var results = str.match(/^(~|~?-?[0-9]+) (~|~?-?[0-9]+) (~|~?-?[0-9]+) ([0-9]{1,3})(?: ([0-9]{1,3}))?/);
if(!results) return false;
return results;
},
action(params) {
var res = params.map((num, i) => { // parseInt parameters
if (num.indexOf('~') == 0) {
return (player.position[['', 'x', 'y', 'z'][i]] >> 5) + parseInt(num.slice(1) || 0);
} else {
return parseInt(num); // return parseInt>>5 if position, not id
}
});
player.setBlock(new Vec3(res[1], res[2], res[3]), res[4],res[5]);
var res = params.slice(1, 4);
res = res.map((val, i) => serv.posFromString(val, player.position[['x','y','z'][i]] / 32))
player.setBlock(new Vec3(res[0], res[1], res[2]).floored(), params[4], params[5] || 0);
}
});
};

View file

@ -1,32 +1,55 @@
var Vec3 = require("vec3").Vec3;
var UserError = require('flying-squid').UserError;
module.exports.player=function(player, serv) {
player.commands.add({
base: 'help',
info: 'to show all commands',
usage: '/help [command]',
action(params) {
var c = params[0];
var hash = player.commands.hash;
parse(str) {
var params = str.split(' ');
var page = parseInt(params[params.length-1]);
var search = '';
if (page) {
params.pop();
}
search = params.join(' ');
return { search: search, page: (page && page - 1) || 0 };
},
action({search, page}) {
if (page < 0) return 'Page # must be >= 1';
var hash = player.commands.uniqueHash;
if(c) {
var f=player.commands.find(c);
if(f==undefined || f.length==0) return 'Command '+c+' not found';
return f[0].params.usage + ' ' + f[0].params.info;
} else {
var used = [];
for(var key in hash) {
if(used.indexOf(hash[key]) > -1) continue;
used.push(hash[key]);
var PAGE_LENGTH = 8;
if(hash[key].params.info && (player.op || !hash[key].params.op)) {
var str = hash[key].params.usage + ' ' + hash[key].params.info;
if(hash[key].params.aliases && hash[key].params.aliases.length) {
str += ' (aliases: ' + hash[key].params.aliases.join(', ') + ')';
}
var found = Object.keys(hash).filter(h => (h + ' ').indexOf((search && search + ' ') || '') == 0);
player.chat(str);
}
if (found.length == 0) { // None found
return 'Could not find any matches';
} else if (found.length == 1) { // Single command found, giev info on command
var cmd = hash[found[0]];
var usage = (cmd.params && cmd.params.usage) || cmd.base;
var info = (cmd.params && cmd.params.info) || 'No info';
player.chat(usage + ': ' + info);
} else { // Multiple commands found, give list with pages
var totalPages = Math.ceil((found.length-1) / PAGE_LENGTH);
if (page >= totalPages) return 'There are only' + totalPages + ' help pages';
found = found.sort();
if (found.indexOf('search') != -1) {
var baseCmd = hash[search];
player.chat(baseCmd.base + ' -' + ((baseCmd.params && baseCmd.params.info && ' ' + baseCmd.params.info) || '=-=-=-=-=-=-=-=-'));
} else {
player.chat('Help -=-=-=-=-=-=-=-=-');
}
for (var i = PAGE_LENGTH*page; i < Math.min(PAGE_LENGTH*(page + 1), found.length); i++) {
if (i == search) continue;
var cmd = hash[found[i]];
var usage = (cmd.params && cmd.params.usage) || cmd.base;
var info = (cmd.params && cmd.params.info) || 'No info';
player.chat(usage + ': ' + info);
}
player.chat('--=[Page ' + (page + 1) + ' of ' + totalPages + ']=--')
}
}
});
@ -83,8 +106,7 @@ module.exports.player=function(player, serv) {
},
action(sel) {
var arr = serv.selectorString(sel, player.position.scaled(1/32), player.world);
if (arr instanceof Error) return arr.toString();
else if (arr == null) return 'Could not find player';
if (arr == null) return 'Could not find player';
else player.chat(JSON.stringify(arr.map(a => a.id)));
}
});
@ -96,11 +118,16 @@ module.exports.player=function(player, serv) {
if (res) player.chat('' + res);
}
catch(err) {
setTimeout(() => {throw err;}, 0);
if (err instanceof UserError) player.chat('Error: ' + err.toString());
else setTimeout(() => {throw err;}, 0);
}
}
};
module.exports.entity = function(entity, serv) {
entity.selectorString = (str) => serv.selectorString(str, entity.position.scaled(1/32), entity.world);
}
module.exports.server = function(serv) {
function shuffleArray(array) {
@ -126,7 +153,7 @@ module.exports.server = function(serv) {
serv.selector = (type, opt) => {
if (['all', 'random', 'near', 'entity'].indexOf(type) == -1)
return new Error('serv.selector(): type must be either [all, random, near, or entity]');
throw new UserError('serv.selector(): type must be either [all, random, near, or entity]');
var count = typeof opt.count != 'undefined' ?
count :
@ -209,12 +236,13 @@ module.exports.server = function(serv) {
else return sample.slice(count); // Negative, returns from end
}
serv.selectorString = (str, pos, world) => {
serv.selectorString = (str, pos, world, allowUser=true) => {
pos = pos.clone();
var player = serv.getPlayer(str);
if (!player && str[0] != '@') return null;
else if (player) return allowUser ? [player] : null;
var match = str.match(/^@([a,r,p,e])(?:\[([^\]]+)\])?$/);
if (match == null) return new Error('Invalid selector format');
if (match == null) throw new UserError('Invalid selector format');
var typeConversion = {
a: 'all',
r: 'random',
@ -227,10 +255,10 @@ module.exports.server = function(serv) {
var err;
opt.forEach(o => {
var match = o.match(/^([^=]+)=([^=]+)$/);
if (match == null) err = new Error('Invalid selector option format: "' + o + '"');
if (match == null) err = new UserError('Invalid selector option format: "' + o + '"');
else optPair.push({key: match[1], val: match[2]});
});
if (err) return err;
if (err) throw err;
var optConversion = {
type: 'type',
@ -269,4 +297,11 @@ module.exports.server = function(serv) {
return serv.selector(type, data);
}
serv.posFromString = (str, pos) => {
if (parseInt(str)) return parseInt(str);
if (str.match(/~-?\d+/)) return parseInt(str.slice(1)) + pos;
else if (str == '~') return pos;
else throw new UserError('Invalid position');
};
}

View file

@ -19,9 +19,8 @@ module.exports.server=function(serv,options) {
}
}
if (!entity.velocity || !entity.size) return;
var oldPosAndOnGround = await entity.calculatePhysics(delta);
if (!oldPosAndOnGround.oldPos.equals(new Vec3(0,0,0)))
if (entity.type == 'mob') entity.sendPosition(oldPosAndOnGround);
var posAndOnGround = await entity.calculatePhysics(delta);
if (entity.type == 'mob') entity.sendPosition(posAndOnGround.position, posAndOnGround.onGround);
})
).catch((err)=> setTimeout(() => {throw err;},0));
});

View file

@ -158,7 +158,7 @@ module.exports.player=function(player,serv)
sendLogin();
await player.sendMap();
player.sendSpawnPosition();
player.sendPosition();
player.sendSelfPosition();
player.updateHealth(player.health);

View file

@ -27,14 +27,14 @@ module.exports.entity=function(entity){
entity.velocity.z = getFriction(entity.velocity.z, entity.friction.z, delta);
}
var oldPos = entity.position.clone();
var newPos = entity.position.clone();
entity.position.x += getMoveAmount('x', xBlock, entity, delta, sizeSigned.x);
entity.position.y += getMoveAmount('y', yBlock, entity, delta, sizeSigned.y);
entity.position.z += getMoveAmount('z', zBlock, entity, delta, sizeSigned.z);
newPos.x += getMoveAmount('x', xBlock, entity, delta, sizeSigned.x);
newPos.y += getMoveAmount('y', yBlock, entity, delta, sizeSigned.y);
newPos.z += getMoveAmount('z', zBlock, entity, delta, sizeSigned.z);
//serv.emitParticle(30, serv.overworld, entity.position.scaled(1/32), { size: new Vec3(0, 0, 0) });
return { oldPos: oldPos, onGround: yBlock}
return { position: newPos, onGround: yBlock}
};

View file

@ -9,7 +9,7 @@ module.exports.player=function(player)
gamemode:player.gameMode,
levelType:'default'
});
player.sendPosition();
player.sendSelfPosition();
player.updateHealth(20);
player.nearbyEntities=[];
player.updateAndSpawn();

View file

@ -2,11 +2,6 @@ var Vec3 = require("vec3").Vec3;
module.exports.player = (player, serv) => {
var getPos = (num, dir='x', p=player) => {
if (num[0] == '~') return p.position[dir] + parseInt(num.slice(1, num.length) || 0)*32;
else return parseInt(num)*32;
}
player.commands.add({
base: 'teleport',
aliases: ['tp'],
@ -14,34 +9,29 @@ module.exports.player = (player, serv) => {
usage: '/teleport [target player] <destination player or x> [y] [z]',
op: true,
parse(str) {
return str.match(/^(((\w* )?~?-?\d* ~?-?\d* ~?-?\d*)|(\w* \w*))$/) ? str.split(' ') : false;
return str.match(/^(((.* )?~?-?\d* ~?-?\d* ~?-?\d*)|(.+ .+))$/) ? str.split(' ') : false;
},
action(args) {
if(args.length === 2 && args[0] !== args[1]) {
let player_from;
let player_to;
if(args.length === 2) {
let entities_from = player.selectorString(args[0]);
let entity_to = player.selectorString(args[1])[0];
if(!(player_from = serv.getPlayer(args[0])) || !(player_to = serv.getPlayer(args[1])))
return false;
player_from.teleport(player_to.position.clone());
entities_from.forEach(e => e.teleport(entity_to.position.scaled(1/32)));
} else if(args.length === 3) {
let x = getPos(args[0], 'x');
let y = getPos(args[1], 'y');
let z = getPos(args[2], 'z');
let x = serv.posFromString(args[0], player.position.x / 32);
let y = serv.posFromString(args[1], player.position.y / 32);
let z = serv.posFromString(args[2], player.position.z / 32);
player.teleport(new Vec3(x, y, z));
} else if(args.length === 4) {
let player_from;
let entities_from = player.selectorString(args[0]);
if(!(player_from = serv.getPlayer(args[0])))
return false;
let x = getPos(args[1], 'x', player_from);
let y = getPos(args[2], 'y', player_from);
let z = getPos(args[3], 'z', player_from);
player_from.teleport(new Vec3(x, y, z));
entities_from.forEach(e => e.teleport(new Vec3(
serv.posFromString(args[1], e.position.x / 32),
serv.posFromString(args[2], e.position.y / 32),
serv.posFromString(args[3], e.position.z / 32)
)));
}
}
});

View file

@ -39,55 +39,20 @@ module.exports.player=function(player)
headYaw: convYaw
});
}, () => {
player.sendPosition();
player.sendSelfPosition();
});
}
player._client.on('position', ({x,y,z,onGround} = {}) =>
player.sendRelativePositionChange((new Vec3(x, y, z)).toFixedPosition(), onGround));
player._client.on('position', ({x,y,z,onGround} = {}) => {
player.sendPosition((new Vec3(x, y, z)).toFixedPosition(), onGround);
});
player._client.on('position_look', ({x,y,z,onGround,yaw,pitch} = {}) => {
player.sendRelativePositionChange((new Vec3(x, y, z)).toFixedPosition(), onGround);
player.sendPosition((new Vec3(x, y, z)).toFixedPosition(), onGround);
sendLook(yaw,pitch,onGround);
});
player.sendRelativePositionChange = (newPosition, onGround) => {
return player.behavior('move', {
onGround: onGround,
position: newPosition
}, async ({onGround, position}) => {
if (player.position.distanceTo(new Vec3(0, 0, 0)) != 0) {
var diff = position.minus(player.position);
if(diff.abs().x>127 || diff.abs().y>127 || diff.abs().z>127)
{
player._writeOthersNearby('entity_teleport', {
entityId:player.id,
x: position.x,
y: position.y,
z: position.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
});
}
}
player.position = position;
player.onGround = onGround;
}, () => {
player.sendPosition();
});
};
player.sendPosition = () => {
player.sendSelfPosition = () => {
player._client.write('position', {
x: player.position.x/32,
y: player.position.y/32,
@ -99,43 +64,46 @@ module.exports.player=function(player)
};
player.teleport = async (position) => {
await player.sendRelativePositionChange(position, false);
player.sendPosition();
var notCancelled = await player.sendPosition(position.scaled(32).floored(), false, true);
if (notCancelled) player.sendSelfPosition();
}
};
module.exports.entity=function(entity,serv){
entity.sendPosition = ({oldPos,onGround}) => {
entity.behavior('move', {
old: oldPos,
onGround: onGround
}, ({old,onGround}) => {
var diff = entity.position.minus(old);
entity.sendPosition = (position, onGround, teleport=false) => {
if (typeof position == 'undefined') throw new Error('undef');
if (entity.position.equals(position) && entity.onGround == onGround) return Promise.resolve();
return entity.behavior('move', {
position: position,
onGround: onGround,
teleport: teleport
}, ({position,onGround}) => {
var diff = position.minus(entity.position);
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,
x: position.x,
y: position.y,
z: 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', {
else if (diff.distanceTo(new Vec3(0, 0, 0)) != 0) entity._writeOthersNearby('rel_entity_move', {
entityId: entity.id,
dX: diff.x,
dY: diff.y,
dZ: diff.z,
onGround: onGround
}, entity);
});
entity.position = position;
entity.onGround = onGround;
}, () => {
entity.position = oldPos;
if (entity.type == 'player') player.sendSelfPosition();
});
};
entity.sendVelocity = (vel, maxVel) => {
var velocity = vel.scaled(32).floored(); // Make fixed point
var maxVelocity = maxVel.scaled(32).floored();
@ -152,6 +120,10 @@ module.exports.entity=function(entity,serv){
}
};
entity.teleport = (pos) => { // Overwritten in players inject above
entity.sendPosition(pos.scaled(32), false, true);
}
function addVelocityWithMax(current, newVel, max) {
var x, y, z;
if (current.x > max.x || current.x < -max.x) x = current.x;

View file

@ -159,7 +159,7 @@ module.exports.player=function(player,serv,settings) {
await player.sendMap();
player.sendPosition();
player.sendSelfPosition();
player.emit('change_world');
await player.waitPlayerLogin();