0.5.1 • Published 2 years ago

trogdord v0.5.1

Weekly downloads
4
License
GPL-3.0
Repository
github
Last release
2 years ago

Node.js Tests

Trogdord Node.js Module

The official Node.js client for trogdord.

Dependencies

Installaton

npm install trogdord

How to use

Establishing a Connection

By default, the Trogdord object will attempt to connect to localhost:1040 (1040 is the default port that trogdord runs on.) If those values are correct, then all you need to do is this:

const connection = new Trogdord();

To specify a different hostname, pass it into the first argument of the constructor:

const connection = new Trogdord('myhostname.com');

Or if your host is an IP address (note that, at the time of this writing, trogdord only supports IPV4):

const connection = new Trogdord('192.168.0.1');

Finally, if trogdord is running on a non-standard port:

const connection = new Trogdord('myhostname.com', 1041);

The constructor also takes a third optional argument for additional settings:

// Connection attempt will timeout in 1 second instead of the default 3.
const connection = new Trogdord('myhostname.com', 1041, {
	connectTimeout: 1000
});

The following options are supported:

  • connectTimeout: the number of milliseconds to wait while attempting to connect to trogdord before timing out

The trogdord module emits the following events while attempting to connect or when the connection is closed:

  • connect: connection has been established
  • close: connection has been closed
  • error: an error occurred while attempting to connect

To make use of the connection after it's established, listen for the connect event:

const connection = new Trogdord();

connection.on('connect', () => {

	// use the connection here
})

To handle errors, listen for the error event:

const connection = new Trogdord();

connection.on('error', error => {

	// Uh oh!
	console.log(error);
})

If you want to trigger a block of code once the connection is closed, listen for the close event:

const connection = new Trogdord();

connection.on('close', () => {

	// cleanup code goes here
})

Retrieving Trogdord Configuration Settings

This method returns the settings trogdord was configured with. Settings that expose security-sensitive information about the server are excluded.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	connection.config()
	.then(stats => {
		console.log(stats);
	}).catch(error => {
		// ...Handle error...
	});
});

Result:

{
  'output.driver': 'local',
  'state.max_dumps_per_game': 0,
  'redis.input_channel': 'trogdord:in',
  'state.enabled': false,
  'redis.output_channel': 'trogdord:out'
}

Retrieving Trogdord Statistics

This method retrieves useful statistical data about the instance of trogdord we're connected to.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	connection.statistics()
	.then(stats => {
		console.log(stats);
	}).catch(error => {
		// ...Handle error...
	});
});

Result:

{
  players: 0,
  version: { major: 0, minor: 29, patch: 0 },
  lib_version: { major: 0, minor: 5, patch: 0 }
}

Dumping Trogdord's State to Disk

This method tells trogdord to dump its state to disk, including all games, and returns no output:

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	connection.dump()
	.catch(error => {
		// ...Handle error...
	});
});

Restoring Trogdord's State from Disk

This method tells trogdord to restore its state from disk, including all previously dumped games, and returns no output:

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	connection.restore()
	.catch(error => {
		// ...Handle error...
	});
});

Retrieving Available Game Definitions

This method retrieves a list of all game definition files that are available to the server:

const connection = new Trogdord();

connection.on('connect', () => {

	connection.definitions()
	.then(definitions => {
		console.log(definitions);
	}).catch(error => {
		// ...Handle error...
	});
});

Result:

[ 'game.xml' ]

Retrieving Games

This method retrieves a list of games that currently exist on the server:

const connection = new Trogdord();

connection.on('connect', () => {

	connection.games()
	.then(games => {
		console.log(games);
	}).catch(error => {
		// ...Handle error...
	});
});

Result (an array of Game objects):

[ Game {} ]

You can also pass an optional list of filters to only return games matching certain criteria. For example, the following code returns only games that are currently running:

const connection = new Trogdord();

connection.on('connect', () => {

	connection.games({is_running: true})
	.then(games => {
		// ...Do something with list of games...
	}).catch(error => {
		// ...Handle error...
	});
});

You can AND more filters together like the following example, which only returns games that are running and whose names start with the prefix "we":

const connection = new Trogdord();

connection.on('connect', () => {

	connection.games({is_running: true, name_starts: "we"})
	.then(games => {
		// ...Do something with list of games...
	}).catch(error => {
		// ...Handle error...
	});
});

If you need OR logic, you can pass in an array of filter groups like the following example, which returns all games that are running OR not running and start with the prefix "we":

const connection = new Trogdord();

connection.on('connect', () => {

	connection.games([{is_running: true}, {is_running: false, name_starts: "we"}]})
	.then(games => {
		// ...Do something with list of games...
	}).catch(error => {
		// ...Handle error...
	});
});

Currently supported filters for game lists:

  • is_running: Takes a boolean value and returns games that are either running or not running
  • name_starts: Takes a string value and returns games whose names start with the given value

Retrieving a Single Game

This method retrieves the game corresponding to a specific id:

const connection = new Trogdord();

connection.on('connect', () => {

	// Retrieving game with id 0
	connection.getGame(0)
	.then(game => {
		console.log(game);
	}).catch(error => {
		// ...Handle error...
	});
});

Result (a Game object):

Game {}

Creating a New Game

This method creates a new game:

const connection = new Trogdord();

connection.on('connect', () => {

	connection.newGame('Game Name', 'game.xml')
	.then(game => {
		console.log(game);
	}).catch(error => {
		// ...Handle error...
	});
});

Result (a Game object):

Game {}

You can also set some initial metadata for the game:

const connection = new Trogdord();

connection.on('connect', () => {

	connection.newGame('Game Name', 'game.xml', {metaKey1: 'value1', metaKey2: 'value2'})
	.then(game => {
		console.log(game);
	}).catch(error => {
		// ...Handle error...
	});
});

The returned instance of Game has the following read-only members:

MemberDescription
idThe game's id
nameThe game's name
definitionThe definition file used to create the game
createdUNIX timestamp of when the game was created
trogdordThe underlying instance of Trogdord that spawned the Dump object

Dumping a Game to Disk

Game.dump() dumps a game to disk.

const connection = new Trogdord();

connection.on('connect', () => {

	connection.newGame('Game Name', 'game.xml')
	.then(game => {
		return game.dump();
	}).then(dump => {
		console.log(dump);
	}).catch(error => {
		// ...Handle error...
	});
});

Result (a Dump object):

Dump {}

Restoring a Dumped Game from Disk

Dump.restore() restores a dumped game from disk.

const connection = new Trogdord();

connection.on('connect', () => {

	// 0 is the dumped game's id
	connection.getDump(0)
	.then(dump => {
		return dump.restore();
	}).then(game => {
		console.log(game);
	}).catch(error => {
		// ...Handle error...
	});
});

You can also pass in an optional slot number to restore a specific slot. See: Restoring a Specific Dumped Game Slot.

Result (a Game object):

Game {}

Getting a Dumped Game's Details

Trogdord.getDumped(id) returns the details of a dumped game.

const connection = new Trogdord();

connection.on('connect', () => {

	// 0 is the dumped game's id
	connection.getDump(0)
	.then(dump => {
		console.log(dump);
	}).catch(error => {
		// ...Handle error...
	});
});

Result (a Dump object):

Dump {}

The returned instance of Dump has the following read-only members:

MemberDescription
idThe dumped game's id
nameThe dumped game's name
definitionThe definition file used to create the game
createdUNIX timestamp of when the game was created
trogdordThe underlying instance of Trogdord that spawned the Dump object

Getting a List of All Dumped Games

Trogdord.dumped() returns an array of Dump instances representing every dumped game.

const connection = new Trogdord();

connection.on('connect', () => {

	connection.dumped()
	.then(list => {
		console.log(list);
	}).catch(error => {
		// ...Handle error...
	});
});

Result (array of Dump objects):

[Dump {},  Dump {}, ...]

Deleting a Dumped Game

Dump.destroy() deletes a dumped game (if the dump has been restored, this does not affect the already existing game.)

const connection = new Trogdord();

connection.on('connect', () => {

	// 0 is the dumped game's id
	connection.getDump(0)
	.then(dump => {
		dump.destroy();
	}).catch(error => {
		// ...Handle error...
	});
});

Retrieving a Specific Dumped Game Slot

The way you version a dumped game is via slots. Every time a game is dumped, a new slot is formed containing that version's game state. You can use the Dump.getSlot(id) method to retrieve a specific slot from a game's dump history.

const connection = new Trogdord();

connection.on('connect', () => {

	// 0 is the dumped game's id
	connection.getDump(0)
	.then(dump => {
		// Every dump has at least one slot with the value 0. Each time a game
		// is dumped, the slot number of the next game is incremented.
		return dump.getSlot(0);
	}).then(slot => {
		console.log(slot);
	}).catch(error => {
		// ...Handle error...
	});
});

Result (a Slot object):

Slot {}

The returned instance of Slot has the following read-only members:

MemberDescription
slotThe slot number
timestampTimestamp (in milliseconds this time, not a UNIX timestamp) of when the slot was dumped
dumpThe underlying instance of Dump that spawned the Slot object

Retrieving a List of All Slots for a Dumped Game

Dump.slots() returns all slots associated with a game's dump history.

const connection = new Trogdord();

connection.on('connect', () => {

	// 0 is the example dumped game's id
	connection.getDump(0)
	.then(dump => {
		return dump.slots();
	}).then(slots => {
		console.log(slots);
	}).catch(error => {
		// ...Handle error...
	});
});

Result (array of Slot objects):

[Slot {},  Slot {}, ...]

Restoring a Specific Dumped Game Slot

Games can be restored from a specific slot either via Dump.restore(slot) or Slot.restore().

Example using Dump.restore(slot):

const connection = new Trogdord();

connection.on('connect', () => {

	// 0 is the dumped game's id
	connection.getDump(0)
	.then(dump => {
		return dump.restore(0);
	}).then(game => {
		// ... Do something with the restored game ...
	}).catch(error => {
		// ...Handle error...
	});
});

Example using Slot.restore():

const connection = new Trogdord();

connection.on('connect', () => {

	// 0 is the dumped game's id
	connection.getDump(0)
	.then(dump => {
		return dump.getSlot(0);
	}).then(slot => {
		return slot.restore();
	}).then(game => {
		// ... Do something with the restored game ...
	}).catch(error => {
		// ...Handle error...
	});
});

Deleting a Specific Dumped Game Slot

Slot.destroy() removes the specified dump slot. If all slots for a given game have been removed, the associated instance of Dump will also be destroyed server-side and invalidated.

const connection = new Trogdord();

connection.on('connect', () => {

	// 0 is the dumped game's id
	connection.getDump(0)
	.then(dump => {
		return dump.getSlot(0);
	}).then(slot => {
		slot.destroy();
	}).catch(error => {
		// ...Handle error...
	});
});

Returning Game-Specific Statistics

Game.statistics() returns statistical data associated with a specific game.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Get an existing game and return its statistics
	connection.getGame(0)
	.then(game => game.statistics())
	.then(response => {
		console.log(response);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Result:

{
  created: '2020-04-21 21:20:50 UTC',
  players: 1,
  current_time: 25,
  is_running: true
}

Checking if a Game is Running

Game.isRunning() will return true if the game is running and false if not.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Get an existing game and check if it's running
	connection.getGame(0)
	.then(game => game.isRunning())
	.then(response => {
		console.log(response);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Getting Current In-Game Time

Game.getTime() will return the current in-time game.

Example:

	// Get an existing game and check its current time
	connection.getGame(0)
	.then(game => game.getTime())
	.then(response => {
		console.log(response);
	})
	.catch(error => {
		// ...Handle error...
	});

Result (means the game timer has been running for 60 seconds):

60

Starting a Game

Game.start() will start a game.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	let game;

	// Create a new game and then start it
	connection.newGame('Game Name', 'game.xml')
	.then(newGame => {
		game = newGame;
		return game.start();
	})
	.then(response => game.isRunning())
	.then(response => {
		console.log(response);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Result:

true

Stopping a Game

Game.stop() will stop a game.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	let game;

	// Get an existing game and stop it
	connection.getGame(0)
	.then(_game => {
		game = _game;
		return game.stop();
	})
	.then(response => game.isRunning())
	.then(response => {
		console.log(response);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Result:

false

Destroying a Game

Game.destroy() will destroy the game on the server side. Once the game has been destroyed, invoking further requests from the same object will result in 404 game not found errors.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Get an existing game and destroy it
	connection.getGame(0)
	.then(game => game.destroy())
	.then(response => {
		// ...Any code that needs to run after game has been destroyed...
	})
	.catch(error => {
		// ...Handle error...
	});
});

By passing in an optional boolean argument to Game.destroy(), we can instruct the server to either delete or not delete all dumps of the game (if any) in addition to the game itself. At the time of this writing, the server's default behavior is to delete the dumps.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Get an existing game and destroy it, but preserve the dump
	connection.getGame(0)
	.then(game => game.destroy(false))
	.then(response => {
		// At this point, the game itself has been destroyed, but any dump that was made has been preserved.
	})
	.catch(error => {
		// ...Handle error...
	});
});

Getting a Game's Metadata

Game.getMeta() returns one or more metadata values associated with a game.

If you don't pass an argument, all metadata values will be returned.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Get an existing game and print all of its associated metadata
	connection.getGame(0)
	.then(game => game.getMeta())
	.then(response => {
		console.log(response);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Result:

{ author: 'James Colannino', title: 'Super Funtime Sample Game', synopsis: "A rootin' tootin' good time!" }

You can also pass in specific metadata keys and only get those values back:

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Get an existing game and print some of its associated metadata
	connection.getGame(0)
	.then(game => game.getMeta(['author', 'title']))
	.then(response => {
		console.log(response);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Result:

{ author: 'James Colannino', title: 'Super Funtime Sample Game' }

Finally, if you only need a specific metadata value, you can skip the array and just pass in a string containing the desired key.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Get an existing game and print a metadata value
	connection.getGame(0)
	.then(game => game.getMeta('author'))
	.then(response => {
		console.log(response);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Result:

{ author: 'James Colannino' }

Setting Game Metadata

Game.setMeta({key: value, ...}) takes as input an object of key, value pairs and sets them as metadata for the game.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Get an existing game with id 0 and set some metadata
	connection.getGame(0)
	.then(game => game.setMeta({key1: 'value1', key2: 'value2'}))
	.then(response => {
		// ... do something once we know the metadata is set...
	})
	.catch(error => {
		// ...Handle error...
	});
});

Getting All Entities in the Game

Game.entities() will return all entities in the game. Fun Fact: Objects are called TObject because naming a JavaScript class "Object" doesn't work so well. Yes, I realized this the hard way.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Get an existing game and return all of its entities
	connection.getGame(0)
	.then(game => game.entities())
	.then(response => {
		console.log(response);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Result:

[
  TObject {},  TObject {},
  TObject {},  TObject {},
  TObject {},  Room {},
  Room {},     Room {},
  Room {},     Creature {},
  Creature {}
]

If you'd like to get back an array of simple objects containing just the name and type of each entity, pass in false for the first optional argument.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Get an existing game and return all of its entities
	connection.getGame(0)
	.then(game => game.entities(false))
	.then(response => {
		console.log(response);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Result:

[
  { name: 'stick', type: 'object' },
  { name: 'boulder', type: 'object' },
  { name: 'rock', type: 'object' },
  { name: 'sword', type: 'object' },
  { name: 'candle', type: 'object' },
  { name: 'mysticalhall', type: 'room' },
  { name: 'start', type: 'room' },
  { name: 'chamber', type: 'room' },
  { name: 'cave', type: 'room' },
  { name: 'trogdor', type: 'creature' },
  { name: 'casper', type: 'creature' }
]

You can also return more specific lists of things. For example, to return all entities that inherit from Thing:

const connection = new Trogdord();

connection.on('connect', () => {

	// Get an existing game and return all of its things
	connection.getGame(0)
	.then(game => game.things())
	.then(response => {
		console.log(response);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Result:

[
  TObject {},  TObject {},
  TObject {},  TObject {},
  TObject {},  Creature {},
  Creature {}
]

And to just return all creatures:

const connection = new Trogdord();

connection.on('connect', () => {

	// Get an existing game and return all of its creatures
	connection.getGame(0)
	.then(game => game.creatures())
	.then(response => {
		console.log(response);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Result:

[
  Creature {},  Creature {}
]

Lists of all entity types can be requested.

Getting a Specific Entity in a Game

Game.getEntity(name) returns a specific entity in the game.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Get the room named 'start' from the game with id = 0
	connection.getGame(0)
	.then(game => game.getEntity('start'))
	.then(response => {
		console.log(response);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Result:

Room {}

You can also request more specific types of entities. For example, Game.getBeing(name) will return either a creature or a player by the given name. A method exists for each entity type.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Get the TObject named 'stick' from the game with id = 0
	connection.getGame(0)
	.then(game => game.getObject('stick'))
	.then(response => {
		console.log(response);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Result:

Object {}

If an entity by the specified name exists in the game but does not match the requested type, an entity not found error will be returned.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// An entity named 'start' exists, but it's a room, not an object.
	connection.getGame(0)
	.then(game => game.getObject('start'))
	.then(response => {
		console.log(response);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Result:

Error: entity not found
    at node_modules/trogdord/lib/game.js:97:18
    at processTicksAndRejections (internal/process/task_queues.js:94:5) {
  status: 404
}

Creating a New Player

Creating a new player in a game can be accomplished by a call to Game.createPlayer(name).

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Create a player in an existing game
	connection.getGame(0)
	.then(game => game.createPlayer('n00bslay3r'))
	.then(player => {
		console.log(player);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Result:

Player {}

Retrieving Entity Output Messages

To retrieve the output messages for an entity (for example, a player), call Entity.output(channel).

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Retrieve a player's output on the 'display' channel
	connection.getGame(0)
	.then(game => game.getPlayer('n00bslay3r'))
	.then(player => player.output('display'))
	.then(output => {
		console.log(output);
	})
	.catch(error => {
		// ...Handle error...
	});
});

Result:

[
  { timestamp: 1587598549, content: 'The Palace\n' },
  { timestamp: 1587598549, content: '\n' },
  {
    timestamp: 1587598549,
    content: "You're standing in the middle of a sprawling white marble castle, the walls lined with portraits of long deceased ancestors of some forgotten king.  Servants are bustling about, rushing around with steaming plates of meats and cooked vegetables, rushing in and out of rooms to serve princes and heads of state. Legend has it that going sideways can transport you to a strange and mystical land.\n" +
      '\n' +
      'To the north, you see a dark hole in the wall big enough to walk through.\n'
  },
  { timestamp: 1587598549, content: '\n' },
  { timestamp: 1587598549, content: 'You see a candle.\n' },
  { timestamp: 1587598549, content: '\n' },
  { timestamp: 1587598549, content: 'You see the Sword of Hope.\n' }
]

This will only work if the output driver trogdord is configured to use supports retrieving messages. If it doesn't (looking at you, redis driver), you'll get an error instead.

Result if trogdord is configured to use the redis output driver:

Error: redis output driver does not support this operation
    at node_modules/trogdord/lib/entity.js:106:19
    at processTicksAndRejections (internal/process/task_queues.js:94:5) {
  status: 501
}

Appending Message to an Entity's Output Stream

Entity.output() can also append a new message to an Entity's output stream provided a second argument is specified.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Send a new message to a player's output stream
	connection.getGame(0)
	.then(game => game.getPlayer('n00bslay3r'))
	.then(player => {
		player.output('notifications', 'You were naughty. Be aware that further abuse will result in a permanent ban.');
	})
	.catch(error => {
		// ...Handle error...
	});
});

Sending a Command to a Player's Input Stream

Player.input(command) will send a command to the player's input stream. Return value will be a promise that resolves to a successful response object.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	// Retrieve a player from an existing game and send a command on its behalf
	connection.getGame(0)
	.then(game => game.getPlayer('n00bslay3r'))
	.then(player => {
		player.input('go north');
	})
	.catch(error => {
		// ...Handle error...
	});
});

Removing (Destroying) a Player

Removing a player can be accomplished with a call to Player.destroy(). Note that, at the time of this writing, no other entity type can be destroyed via a trogdord request.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	let player;

	// Remove an evil player from a game
	connection.getGame(0)
	.then(game => game.getPlayer('n00bslay3r'))
	.then(_player => {
		player = _player;
		return player.output('notifications', "You've been a bad monkey and are now banned.");
	})
	.then(response => {
		player.destroy();
	})
	.catch(error => {
		// ...Handle error...
	});
});

Making a Raw JSON Request

Raw JSON requests are a low level mechanism that should, under ordinary circumstances, be made only by class methods whose underlying implementations are abstracted from the client. Nevertheless, you might run into a situation where making a raw request is advantageous or even necessary, and for this reason, the makeRequest method exists.

Example:

const connection = new Trogdord();

connection.on('connect', () => {

	connection.makeRequest({
		method: "get",
		scope: "global",
		action: "statistics"
	}).then(response => {
		// ...Do something with the JSON response...
	}).catch(error => {
		// ...Handle error...
	});
});

By default, the request timeout is three seconds, but you can change that by passing in a different value (in milliseconds) as an optional second argument to makeRequest:

const connection = new Trogdord();

connection.on('connect', () => {

	// Request times out in half a second instead of three
	connection.makeRequest({
		method: "get",
		scope: "global",
		action: "statistics"
	}, 500).then(response => {
		// ...
	}).catch(error => {
		// ...
	});
});

Checking if a Connection is Open or Closed

Although instances of Trogdord are constructed with open connections, it's possible that the connection will be closed at some point. To check if the connection is open or closed, read the Trogdord.connected property:

const connection = new Trogdord();

connection.on('connect', () => {

	// Will output boolean true
	console.log(connection.connected);

	connection.close();

	// Will output boolean false
	console.log(connection.connected);
}

Checking the Status of the Last Request

After any request to trogdord, the Trogdord.status getter will return the status returned as part of the server's response:

const connection = new Trogdord();

connection.on('connect', () => {

	connection.statistics().then(response => {

		// Since the request was successful, this should output 200
		console.log(connection.status);
	}).catch(error => {

		// Will output the status of our unsuccessful request to get:global:statistics
		console.log(connection.status);
	});
});
0.5.1

2 years ago

0.5.0

3 years ago

0.4.0

4 years ago

0.3.0

4 years ago

0.2.0

4 years ago

0.1.3

4 years ago

0.1.2

4 years ago

0.1.1

4 years ago

0.1.0

4 years ago