haxilium v0.8.8
Haxilium
WARNING. This npm package is written in TypeScript. If you want to use older version which is written in JavaScript run
npm install haxilium@0.7.1
Haxball Headless API Framework for easy and organized development. Haxilium requires TypeScript to work. This provides strict typechecking and helps to avoid a lot of bugs on compile time.
import haxilium, { Player } from 'haxilium'
const room = haxilium({ roomName: 'Haxilium Room' })
room.onPlayerJoin = function (player: Player) {
room.sendChat(`Hello, ${player.name}!`)
}
Installation
Use following command to install Haxilium:
npm install haxilium
That's all!
Getting started
Haxilium provides the same API as Haxball Headless API but also adds modules and custom Player
models.
Create a room
Use haxilium(config: RoomConfig)
to create a room:
import haxilium from 'haxilium'
const room = haxilium({
roomName: 'Haxilium Room',
playerName: 'Haxilium Bot',
maxPlayers: 10,
public: true,
geo: { code: 'en', lat: 52, lon: 0 }
})
The above code will create a public haxball room with "Haxilium Room" name , "Haxilium Bot" player(bot) name, maximum amount of players of 10 and with geolocation of England.
RoomConfig
RoomConfig
is the same as in Haxball Headless API. You can look at description of room config here.
Custom Player
model
In addition, Haxilium provides more RoomConfig
properties:
Player
- class which extendshaxilium.Player
class. To fire events when any player's field is changed, you have to decorate it withEvent(event: string)
decoratorroles
- an object of roles. E.g.,{ ingame: 0, moderator: 1, admin: 2 }
getRoles(player: Player)
- a function which returns an array of strings: allroles
of theplayer
You will need roles to limit access to specific commands. Command creation will be explained later.
Example:
import haxilium, { Event, Team, Player as PlayerBase } from 'haxilium'
class Player extends PlayerBase {
@Event('playerCustomFieldChange') customField = false
}
const room = haxilium({
Player: Player,
roles: { customRole: 0, ingame: 1, admin: 2 },
getRoles: (player: Player) => [
player.admin ? 'admin' : '',
player.team !== Team.Spect ? 'ingame' : '',
player.customField ? 'customRole': '',
]
})
room.onPlayerJoin = function (player: Player) {
// This line of code will fire 'playerCustomFieldChange' event.
player.customField = true
}
room.onPlayerCustomFieldChange = function (player: Player) {
console.log('player.customField was changed')
}
Attach callbacks
To attach callback to, for example, playerJoin
event, use the following code:
room.onPlayerJoin = function (player: Player) {
console.log(player.name + ' has joined')
}
When you don't want to use the callback anymore you can delete
it or set it to any falsy value:
// Recommended.
delete room.onPlayerJoin
// This is NOT recommended.
room.onPlayerJoin = null
room.onPlayerJoin = undefined
room.onPlayerJoin = false
room.onPlayerJoin = 0
room.onPlayerJoin = ''
To see full list of events visit this page.
Custom events
To fire a custom event, use Room.dispatchEvent(event: string, args: any[])
:
const room = haxilium({ ... })
room.onPlayerJoin = function (player: Player) {
room.dispatchEvent('customPlayerJoin', ['Hello there'])
}
room.onCustomPlayerJoin = function (message: string) {
// message is "Hello there"
console.log(message)
}
Improved Room.getPlayerList()
There are 3 improvements in Room.getPlayerList()
method:
- It will never return player with ID = 0. In other words, it will never return host player
You can sort players by team by passing an array of team IDs as first argument:
// Team.Red, Team.Blue and Team.Spect are 1, 2, 0 respectively. const teams = room.getPlayerList([Team.Red, Team.Blue, Team.Spect]) const red = teams[0] const blue = teams[1] const spect = teams[2]
Or use ES6 destructuring assignment:
const [red, blue, spect] = room.getPlayerList([Team.Red, Team.Blue, Team.Spect])
In result,
red
will contain only players from red team,blue
will contain only players from blue team andspect
will contain only spectators.You can filter out players by passing filter object as first argument(or second argument if you also want to sort players by team):
const admins = room.getPlayerList({ admin: true }) const [redAdmins, blueAdmins] = room.getPlayerList([Team.Red, Team.Blue], { admin: true })
Module system
Introduction
Module is a class which is decorated with Module()
decorator and it defines callbacks and commands as its methods:
import { Module, Player } from 'haxilium'
@Module()
class LoggingModule {
private logs: string[] = []
onPlayerJoin(player: Player) {
this.logs.push(`${player.name} has joined`)
}
onPlayerLeave(player: Player) {
this.logs.push(`${player.name} has left`)
}
}
Then you pass all your modules to the modules
field in the RoomConfig
:
const room = haxilium({
roomName: 'Room with modules',
modules: [LoggingModule]
})
You can also define methods, fields and other stuff in the module class but I recommend not to use names which start with on
to avoid collisions with callbacks.
Access room in a module
To access the room object in the module, you have to define a constructor, which accepts a Room
argument:
import { Module, Room, Player } from 'haxilium'
@Module()
class GreetingModule {
private $: Room
constructor($: Room) {
this.$ = $
}
onPlayerJoin(player: Player) {
this.$.sendChat(`Welcome, ${player.name}!`)
}
}
Notice that
private $: Room
constructor($: Room) {
this.$ = $
}
can be rewritten as
constructor(private $: Room) { }
So the final version of GreetingModule
will be:
import { Module, Room, Player } from 'haxilium'
@Module()
class GreetingModule {
constructor(private $: Room) { }
onPlayerJoin(player: Player) {
this.$.sendChat(`Welcome, ${player.name}!`)
}
}
If you want to use custom player in your module, you have to pass the Player
to the Room
type annotation:
// Player.ts
import { Player as PlayerBase } from 'haxilium'
export class Player extends PlayerBase {
customField = false
}
// index.ts
import { Module, Room } from 'haxilium'
import { Player } from './Player.ts'
@Module()
class GreetingModule {
// Here is the change.
constructor(private $: Room<Player>) { }
onPlayerJoin(player: Player) {
this.$.sendChat(`Welcome, ${player.name}!`)
}
}
Dependency injection
Sometimes, a module can require other modules as its dependencies. For example, NotifierModule
can require PrettyChatModule
. It is not good to create modules by hand:
@Module()
class PrettyChatModule {
constructor(private $: Room) { }
sendChatPretty(message: string) {
const prettyMessage = prettify(message)
this.$.sendChat(message)
}
}
@Module()
class NotifierModule {
constructor(private $: Room) { }
notifyPlayers() {
// Here I want to use `PrettyChatModule.sendChatPretty()`.
// DON'T DO THIS. It is just for demonstration purposes.
// The right way of requiring `PrettyChatModule` is explained down there.
const prettyChat = new PrettyChatModule(this.$)
prettyChat.sendChatPretty('Some notification message')
}
}
The above way of requiring another module as a dependency is bad because if we want to use the same dependency in two different modules we have to create a lot of instances of the same dependency:
@Module() class Dep { someDepMethod() { } }
@Module() class A {
someMethod() {
// First instance.
new Dep().someDepMethod()
}
}
@Module() class B {
someMethod() {
// Second instance.
new Dep().someDepMethod()
}
}
That's why Haxilium provides dependency injection (DI) for modules. To require a module, declare a constructor, which accepts that module as a parameter(like private $: Room
):
@Module()
class PrettyChatModule {
constructor(private $: Room) { }
sendChatPretty(message: string) {
const prettyMessage = prettify(message)
this.$.sendChat(message)
}
}
@Module()
class NotifierModule {
// Define it here.
constructor(private prettyChat: PrettyChatModule) { }
notifyPlayers() {
// Use everywhere in the module.
this.prettyChat.sendChatPretty('Some notification message')
}
}
This way of requiring modules as dependencies will guarantee that every module is created only once.
Add command
To add a command, decorate a method with Command(names: string|string[])
decorator. The method must accept two parameters:
player: Player
- player who executes the commandargs: string[]
- an array of arguments
Example:
import { Module, Command, Player, Room } from 'haxilium'
@Module()
class LoggingModule {
private logs: string[] = []
constructor(private $: Room) { }
onPlayerJoin(player: Player) {
this.logs.push(`${player.name} has joined`)
}
onPlayerLeave(player: Player) {
this.logs.push(`${player.name} has left`)
}
@Command('printlogs')
pringLogs(player: Player, args: string[]) {
const len = parseInt(args[1]) || 5
const latestLogs = this.logs.slice().reverse().slice(0, len)
for (const log of latestLogs) {
this.$.sendChat(log, player.id)
}
}
}
The above command will send len
latest logs to the chat.
Also, you can define more than one name for a command:
@Module()
class LoggingModule {
...
@Command(['printlogs', 'getlogs'])
pringLogs(player: Player, args: string[]) { ... }
...
}
Names of commands are case insensitive: kick
, Kick
and KiCK
are equal.
Execute command
To execute command, use Room.executeCommand(player: Player, command: string)
. Command will be parsed and passed to the appropriative method. Examples of parsed commans:
printlogs 1
=>['printlogs', '1']
printlogs 1 2
=>['printlogs', '1', '2']
printlogs "1 2"
=>['printlogs', '1 2']
printlogs "1 \" 2"
=>['printlogs', '1 " 2']
As you can see, the name of the command is always the first argument.
Now, use Room.executeCommand()
. A message which starts with !
will be interpreted as a command:
room.onPlayerChat = function (player: Player, message: string) {
if (message[0] === '!') {
// Remove the leading '!'.
const command = message.substring(1)
return room.executeCommand(player, command)
}
}
If command does not exist, UnknownCommandError
will be thrown, so it is good to catch that error:
import { UnknownCommandError } from 'haxilium'
room.onPlayerChat = function (player: Player, message: string) {
if (message[0] === '!') {
// Remove the leading '!'.
const command = message.substring(1)
try {
return room.executeCommand(player, command)
} catch (err) {
if (err instanceof UnknownCommandError) {
// Notify player that command does not exist.
room.sendChat(err.message, player.id)
} else {
// Rethrow it.
throw err
}
}
}
}
Limit access to the command
Often you want to limit access for specific commands. For example, only admins can kick players. So, you have to define roles and which player belongs to each role and then pass a second argument (boolean expression string) to the Command()
decorator:
import haxilium, { Module, Command, Room, Player } from 'haxilium'
@Module()
class KickModule {
constructor(private $: Room) { }
@Command('kick', '>=admin')
kickPlayer(byPlayer: Player, args: stirng[]) {
const id = parseInt(args[1])
const reason = args[2]
this.$.kickPlayer(id, reason)
const kickedPlayer = this.$.getPlayer(id)
this.$.sendChat(`${kickedPlayer.name} was kicked by ${byPlayer.name}`)
}
}
const room = haxilium({
roles: { ingame: 0, admin: 1 }
getRoles: (player: Player) => [
player.admin ? 'admin' : '',
player.team !== Team.Spect ? 'ingame' : '',
],
modules: [KickModule],
})
Now, kick
command will be available only to players who belongs to admin
role. Usage:
kick 1
- kick a player with id1
kick 1 "very long afk"
- kick a player with id1
and specify "very long afk" reason
A second parameter of the @Command()
decorator is a string which is a boolean expression. Available operators:
==
,!=
>
,>=
<
,<=
||
,&&
()
- parenthesis
For example:
>ingame && <admin
will allow command execution only for players whose role is greater thaningame
AND less thanadmin
<ingame || >admin
will allow command execution only for players whose role is less thaningame
OR greater thanadmin
.<ingame || (>ingame && <admin)
will allow command execution only for players whose role is either- less than
ingame
OR - greater than
ingame
AND less thanadmin
- less than
If player does not have enough rights to execute command, AccessToCommandDeniedError
will be thrown. It is good to handle this:
import { UnknownCommandError, AccessToCommandDeniedError } from 'haxilium'
room.onPlayerChat = function (player: Player, message: string) {
if (message[0] === '!') {
// Remove the leading '!'.
const command = message.substring(1)
try {
return room.executeCommand(player, command)
} catch (err) {
if (err instanceof UnknownCommandError) {
// Notify player that command does not exist.
room.sendChat(err.message, player.id)
} else if (err instanceof AccessToCommandDeniedError) {
// Notify player that he does not have rights to execute this command.
room.sendChat("You don't have enough rights to execute this command", player.id)
} else {
// Rethrow it.
throw err
}
}
}
}
Meta information about command
Sometimes, there are situations when you want to store some additional information about command. For examlpe, you want to make a help
command, which shows players a description
of each command. To store meta information about command, pass it as third argument to the Command()
decorator and later retrieve it using Room.getCommandMeta(name: string)
:
@Module()
class CommandsModule {
constructor(private $: Room) { }
// Access string can be empty if you want command to be accessible to any player.
@Command('leave', '', {
description: 'Leave the room'
})
makePlayerLeave(player: Player, args: string[]) {
this.$.kickPlayer(player.id, 'Bye!')
}
@Command('help', '', {
description: 'Use `help <command>` to get help for specific <command>'
})
getHelp(player: Player, args: string[]) {
const commandName = (args[1] || '').toLowerCase()
const meta = this.$.getCommandMeta(commandName)
if (meta && meta.description) {
this.$.sendChat(meta.description)
} else {
this.$.sendChat(`Help for "${commandName}" command is unavailable`)
}
}
}
Command meta has any
type. It's your responsibility to check its type when you try to read it.
Afk module example
Below you can see example of an afk module:
import haxilium, { Module, Command, Event, Room, Player as PlayerBase } from 'haxilium'
class Player extends PlayerBase {
@Event('playerAfkChange') afk = false
}
@Module()
class AfkModule {
constructor(private $: Room<Player>) { }
@Command('afk')
setAfk(player: Player, args: string[]) {
player.afk = true
}
@Command(['back', 'here', 'notafk'])
unsetAfk(player: Player, args: string[]) {
player.afk = false
}
onPlayerAfkChange(player: Player) {
if (player.afk) this.$.sendChat(`${player.name} is afk`)
else this.$.sendChat(`${player.name} is not afk`)
}
}
const room = haxilium({
roomName: 'Room with afk command',
Player: Player,
modules: [AfkModule],
})
// Execute command
room.onPlayerChat = function (player: Player, message: string) {
if (message[0] === '!') {
const command = message.substring(1)
try {
return room.executeCommand(player, command)
} catch (err) {
if (err instanceof UnknownCommandError) {
room.sendChat(err.message, player.id)
} else if (err instanceof AccessToCommandDeniedError) {
room.sendChat("You don't have enough rights to execute this command", player.id)
} else {
throw err
}
}
}
}
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago