Plugins for plaiframe
======================

Changes
--------

Functions:
- plugin.<action>
- plugin.connect(settings)
- plugin.disconnect()

- plugin.item.update() - wenn daten/settings sich ändern
- plugin.item.connect(settings) - nur wenn Button getriggert
- plugin.item.disconnect()

Legacy
------

Plugins expand what can be build with Plaitools. They connect the plaiframe to remote software and hardware or add new ways of interconnecting things.

Each plugin has a node module that becomes part of the plaiframe server. 

All plugins can be fund in the ./plugin directory and (for now) are all packaged with plaiframe. That saying, using plugins or not when designing is, first of all, a way to keep things manageable.

----------

1. [Create a Plugin](#create-a-plugin)

1. [Example](#example)

1. [Custom Item Objects](#custom-item-objects)

1. [The Schema](#the-schema)

1. [Functions](#functions-1)

1. [Events](#events)

----------


Create a Plugin
-----------------

In order for plaiframe to find the module, it needs to be in a folder named after the plugin.
This folder then needs to be added to the plaiframe plugin directory.

Inside this folder, make a file called `index.js` (or name it after the plugin). There can of course be more files, but this one is obligatory.

Plaiframe will [require](https://nodejs.org/api/modules.html#modules_require_id) all plugins on startup. To make a plugin work within a [game](https://plaitools.gitlab.io/documentation/plaiframe/module-game-Game.html) it needs to allow access to a certain set of functions. 

These functions can be directly assigned to [module.exports](https://nodejs.org/api/modules.html#modules_module_exports) or the plugin can export a class with the name `Plugin` that inherits the necessary functions. 

If the plugin exports a class, each [game](https://plaitools.gitlab.io/documentation/plaiframe/module-game-Game.html) will create it's own instance.

 Another important part of a plugin is it's JSON Schema. It provides detailed information on what a game designer can do using the plugin.
 
 The schema will determine what data the plugin module is provided with through the functions.
 
 Find out how plugins are assembled and read about [functions](#functions), the [schema](#schema) etc. or checkout an [example](#example).


Example
-------

- [dependencies](#dependencies)
- [functions](#functions)
- [listener](#listener)
- [collections](#collections)
- [adjust the schema](#adjust-the-schema)
- [variables](#variables)
    
For the following example of a simple audio player plugin, you will find a folder called `sound` in `/plugins`. The important files are `sound.js` and `schema.json`. That's where you'll find the context for below Code examples.

Let's build the sound plugin from scratch. I hope it helps to get an understanding of what a new plugin might look like and how it is integrated into plaiframe.

The following chapters will highlight the script parts that are essential for creating a plugin and walk you through the schema properties `settings`, `cue` and `collections`.

> Before you start building a plugin, please checkout a new plaiframe branch.

### dependencies

[play-sound](https://www.npmjs.com/package/play-sound) is a node audio player the sound plugin is built upon. It should become part of the plaiframe package installer, so we need to install it using npm.

`$ npm install play-sound --save-optional`

For packages that are only required by your plugin use the --save-optional option to place it in the optionalDependencies property of package.json. This way the package can be skipped during install if the plugin will not be used.

Now let's edit the `sound.js` file. 

Import the play-audio package and your `schema.json` file. Then create a Sound class and export it as `Plugin`. 

> I like the [class](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) keyword because it creates a visible hierarchy among namespaces. The same thing is of course possible using only the function keyword.

``` javascript

const player = require('play-sound')(opts = {})
var schema = require('./schema.json')

class Sound {}

module.exports.Plugin = Sound
```

For most plugins it will be useful to import the [plugin](https://plaitools.gitlab.io/documentation/plaiframe/module-plugin.html) module. Make your class inherit from [Plugin](https://plaitools.gitlab.io/documentation/plaiframe/plugins/module-plugin-Plugin.html) so the Sound class is equipped with the basics.

``` javascript
const player = require('play-sound')(opts = {})
var schema = require('./schema.json')
const plugin = require('../plugin.js')

class Sound extends plugin.Plugin {
  constructor() {
    log.debug("sound plugin", "Initializing")
  }
}

module.exports.Plugin = Sound
```

> For logging please use the global plaiframe `log` function. Log levels are:
error, warn, info and debug. Provide the plugin name as first argument so it's easier to track down the log source. E.g.:
>
> `log.info("sound plugin", "hello plaiframe")`

### functions

Add a constructor, a setup, an update and a cue function to Sound. Use [super](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes#Super_class_calls_with_super) to call `plugin.Plugin`s constructor and setup functions. If you use one or more collections, Plugin will then be able to create the necessary database entries and references.

``` javascript
class Sound extends plugin.Plugin {
  constructor() {
    super(schema)
  }
  
  setup(config) {
    super.setup(config)
  }
  
  update(config) {
    
  }
  
  cue(data, session) {
    
  }
}
```

#### setup and update

setup is called with a bunch of information and interfaces to the game the plugin was added to. See [Plugin.setup()](https://plaitools.gitlab.io/documentation/plaiframe/plugins/module-plugin-Plugin.html#setup) for details. For now we will only use our own settings that might have been restored on game start and forward them to the update function.

``` json
"settings":{
  "type":"object",
  "title":"Sound Settings",
  "properties":{
    "path":{
      "type":"string"
    }
  }
}
```

As you can see in the example schema, settings has a property `path` that defines what directory soundfiles will be played from. Store the path on `update`.

Now whenever the path is changed in plaiframe editor, `update` is called and the path variable is adjusted.

Finally, to let plaiframe know what the plugins schema looks like, the setup function has to return your schema.

``` javascript
  setup(config) {
    super.setup(config)
    this.update(config.settings)
    return schema
  }
  
  update(settings) {
    if(settings.hasOwnProperty("path")) {
      this.path = settings.path
    }
  }
```

> setup may also return a promise. Make sure to only return once everything is ready to go.

#### cue

If during a [session](https://plaitools.gitlab.io/documentation/plaiframe/module-session-Session.html) a cue is dispatched that has a `sound` action in it, the cue function will be called with whatever properties were inserted.
It has to act upon the options that the sound plugin schema provides.

``` json
"cue":{
  "sound":{
    "type":"object",
    "listener":true,
    "properties":{
      "play":{
        "oneOf": [
          {
            "title":"choose file",
            "type":"string"
          },
          {
            "title":"free text",
            "type":"string"
          }
        ]
      },
      "next":{
        "type":"string"
      },
      "loop":{
        "type":"integer",
        "minimum":-1
      },
      "stop":{
        "type":"string"
      }
    }
  }
}
```

If there are asynchronous things happening in any other case, you need to return a promise.

``` javascript
  cue(data, session) {
    return new Promise((resolve, reject) => {
      if(data.sound.hasOwnProperty("play")) {
        /** play a soundfile */
        player.play(this.path + data.sound.play)
        
        return resolve(undefined)
      }
      else if(data.sound.hasOwnProperty("stop")) {
        /* stop the sound player and cancel the monitoring*/
        
        return resolve(undefined)
      }
    })
  }
```
data will be handed over the way it was structured in the plugin schema. In this case it will have a sound property that might contain `play`,`stop`, `next` and/or `loop`.

cue has to return or resolve once, and not before, it's done with it's business to make sure actions happen in the right order and don't get stuck.

### listener

The sound plugin allows to trigger a `next` cue once a soundfile finished playing. To make plaiframe recognize a listener was established, `cue` has to return a cancel function. In this case the function will simply store a variable that playing was canceled for this audiofile to prevent it from dispatching the next cue.

> Returning a string value would result in triggering another cue immediately. If you don't want plaiframe to do anything return `undefined`.

``` javascript
cue(data, session) {
  return new Promise((resolve, reject) => {
    if(data.sound.hasOwnProperty("play")) {
      /* play a soundfile*/
      player.play(this.path + data.sound.play)
       
      if(data.sound.hasOwnProperty("next")) {
        /* return cancel function for "done playing" listener */
        return resolve( () => {
          this.audio[session.cue]["canceled"] = true
        })
      }
      return resolve(undefined)
    }
    else if(data.sound.hasOwnProperty("stop")) {
      /* stop the sound player and cancel the monitoring */
    }
  })
}
```

With the `session` argument comes a function that will dispatch a cue when used. It is called `callback` and takes a string (the cuename) as argument.

The audio player `play` function takes a callback for when it's done playing the audiofile. If `next` is part of cue data, we can use this callback to run the session callback function. Now, whenever the audioplayer is done playing, the next cue is dispatched.

``` javascript
/* play a soundfile and dispatch next cue when done */
player.play(this.path + data.sound.play, err => {
  if (err) return reject(err)
  if(data.sound.hasOwnProperty("next")) {
    session.callback(data.sound.next)
  }
})
```
Plugins can technically run the session callback at any moment. That is why it is so important to prevent any further use of the function after the cancel function is called.

### collections

Many Plugins allow to create instances of similar Items that for example represent remote hardware or software objects.

To elaborate our example we will add audio playlists that summarize several audio files.

``` json
{
  "collections": {
    "playlist":{
      "type":"object",
      "title":"Playlist",
      "properties":{
        "tracks":{
          "type":"array",
          "required":true,
          "minItems":1,
          "items":{
            "type":"string"
          }
        },
        "settings":{
          "type":"object",
          "required":true,
          "default":{"silence":0.0},
          "properties":{
            "silence":{
              "type":"number",
              "description":"Define a silence between each track in seconds"
            }
          }
        }
      }
    }
  }
}
```

Based on the schema, `Plugin.setup` will create a new collection object and add it to your plugin class. Once a designer creates items in this collection they are accessible through the `items` keyword in the collection object.

``` javascript
for(let item in this.playlist.items) {
  log.info("sound plugin", "name: " + this.playlist.items[item].name + ". id: " + this.playlist.items[item]._id)
}

```
In our example the playlist is implemented as an alternative to submitting a single audio file. If the sound plugin matches a playlist with what's behind the `play` property it will play the playlists tracks one after another.

Check for a matching playlist:
``` javascript
if(this.playlist.items[data.sound.play]) {
  playlist = this.playlist.items[data.sound.play]
}
```

Pick the current track in the playlist to be played as audiofile:
``` javascript
play(playlist, session, options) {
  let audiofile = playlist.tracks[options.progress]
  options.progress ++
  log.debug(this.name, "play sound " + this.path + audiofile)
  /* play audiofile and repeat function if there is more in the playlist */
}
```
For most plugins it is necessary to have more control over changes in collections. Creating a custom Item class is probably the most efficient way to go. See the chapter on [custom item objects](#custom-item-objects) for more complex Collections. 

### adjust the schema

At certain points it might be useful to extend your schema with additional options or restrictions.

As for our sound example, it will be useful to provide a list of all soundfiles that are available.

Plaiframe contains an adapted [file](https://plaitools.gitlab.io/documentation/plaiframe/module-file.html) module based on the [fs-extra npm package](https://www.npmjs.com/package/fs-extra). 

``` javascript
const file = require('../../file.js')
```

Let's use it's `ls()` function to scan the audio directory for mp3 and wav files.

The json schema `enum` list property specifies what elements can be selected. Add an enum with the directory content to the schema `play` property. This will create a dropdown selector in the editor. 

As you can see the schema `play` property contains the `oneOf` keyword. It makes sense now, since we want to keep the option of free text input. So we will only replace the first of two possible play schemas.

``` javascript
update(settings) {
  if(settings.hasOwnProperty("path")) {
    this.path = settings.path
  }
  
  this.schema.cue.sound.properties.play.oneOf[0]["enum"] = file.ls(this.path, [".mp3",".wav"])
  this.schemaUpdate(schema)
}
```
Whenever the schema was adjusted, use the `Plugin.schemUpdate()` function to let plaiframe know.

### variables

In plaiframe variables are defined by using two square brackets left and right. When writing a plugin you don't need to worry about the style. But you need to make sure, that you ckeck for variables wherever you allow for variable content.

With `cue` comes an instance of the plaiframe [Variable class](https://plaitools.gitlab.io/documentation/plaiframe/module-variables-Variables.html) that is bound to the session space. It provides several functions to resolve variables.

Most straight forward is the `review()` function. It tries to replace all variables in a string. It will also replace variables that resolve to an integer or number type.

Maybe the soundfile that is suposed to be played in a cue, depends on some previous events. Use `session.variables.review()` to resolve whatever comes inside the `play` property. In that way you don't miss variable soundfile names.

Since `review()` checks the database for references, it returns a promise once it's done. Wait for it, assign the play property, then resume with your cue actions.

``` javascript
if(data.sound.hasOwnProperty("play")) {
  session.variables.review(data.sound.play)
  .then(result => { 
    data.sound.play = result
    /* play soundfile and check status ... */
  })
}
```

> If you are not sure what datatype you are checking and want to make sure you replace variables in arrays or nested objects too, use `findAndReplace()` instead. Find more ways of working with variables in the [Variable class](https://plaitools.gitlab.io/documentation/plaiframe/module-variables-Variables.html).

Custom Item Objects
--------------------

The plugin `Item` class is merely a representation of the data that was stored by a designer.

I recommend using your own Item classes if you need to perform specific actions on changes during the design process.

Adjust the [colllections add](https://plaitools.gitlab.io/documentation/plaiframe/plugins/module-plugin-Collection.html#add) functions to create instances of your own class with each new item.

Make sure to wait for the Plugin setup function to return. That's when the collections are created. Then overwrite the [colllections add](https://plaitools.gitlab.io/documentation/plaiframe/plugins/module-plugin-Collection.html#add) function, create a new Item instance and pass the necessary arguments:

``` javascript
  setup(config) {
    this.update(config.settings)
    super.setup(config, {custom_items:true})
    .then(result => {
      this.mycollection.add = data => {
        this.mycollection.items[data.name] = new MyCustomItem(data, this.db)
      }
      
      this.mycollection.collectItems()
    })
    
    return schema
  }
```
> You might also want to adjust the plugin.Collection [remove](https://plaitools.gitlab.io/documentation/plaiframe/plugins/module-plugin-Collection.html#remove) or [command](https://plaitools.gitlab.io/documentation/plaiframe/plugins/module-plugin-Collection.html#command) function.

For most use cases it makes sense to adjust the [reconnect](https://plaitools.gitlab.io/documentation/plaiframe/plugins/module-plugin-Item.html#reconnect) function of your Item. It is called once the settings property of your item has changed. [reconnect](https://plaitools.gitlab.io/documentation/plaiframe/plugins/module-plugin-Item.html#reconnect) provides the updated settings as argument.

The Schema
-----------

The JSON Schema defines how and where plaiframe integrates the plugin and how it's functionality can be used. The Schema may change after the plugin is added to a game. The plugin should always feed any changes made to the schema back to plaiframe.

If you extend the Plugin class use schemaUpdate(schema) function with the updated schema.

Check the official website for more information about [JSON Schemas](https://json-schema.org/).

The schema can have following main properties.

- [settings](#settings)
- [cue](#cue-1)
- [collections](#collections-1)
- [events](#events)

### settings

Define what can or has to be set in order to configure the plugin.

The settings property determines what data plaiframe might hand over when calling the plugins `update` function.

The following schema allows the user to change the path to audiofiles used in the sound module.

``` json
"settings":{
  "type":"object",
  "title":"Sound Settings",
  "properties":{
    "path":{
      "type":"string"
    }
  }
}
```

### cue

Define how the plugin is represented in the level editor. 

The cue schema determines what is forwarded to the plugins `cue` function.

A plugin with following schema will add a new property `sound` to each cue.

``` json
"cue":{
  "sound":{
    "type":"object",
    "properties":{
      "play":{
        "type":"string"
      },
      "next":{
        "type":"string"
      },
      "loop":{
        "type":"boolean"
      },
      "stop":{
        "type":"string"
      }
    }
  }
}
```

The **`next`** property has to be of type `string`. It can be anywhere in the cue schema and may only be used to trigger another cue using the session callback function.

### collections

Define what items can be created and configured using the plugin.

Based on each property in collections, plaiframe will create a database collection with the same name where it stores a document for each item.

Each collection item will automatically have a `name`, an `_id`, and a `modified` property once created.

If you add a schema for `settings`, the items [reconnect](https://plaitools.gitlab.io/documentation/plaiframe/plugins/module-plugin-Item.html#reconnect) function will be called once they are changed.

### events

If the plugin dispatches session or game events they need to be defined in the schema. This way they are accessible in the editor.

Events need a name and may have a data load. Use json schema syntax to specify the expected event load.

``` json
{
  "events":{
    "myEvent":{
      "type":"string",
      "enum":["foo","bar","baz"],
      "description":"is triggered on some occasion and will be any one of these strings."
    },
    "anotherEvent":{
      "type":"object",
      "properties":{
        "foo":{
          "type":"string"
        },
        "bar":{
          "type":"number"
        }
      },
      "description":"is triggered on another occasion and will be an object with 2 properties. A string and a number."
    }
  }
}
```

See below on how plugins can dispatch events.


Functions
----------
- [setup](#setup)
- [update](#update)
- [cue](#cue-2)
- [command](#command)

In order for plaiframe to integrate the plugin it has to provide a set of functions. They need to be made accessible through `module.exports` seperately

``` javascript
module.exports = {
  setup:mySetup,
  update:myUpdate,
  cue:myCue,
  command:myCommand
}
```

or packed in a Plugin class

``` javascript
class MyPlugin {
  constructor() {
    
  }
  setup() {
    
  }
  cue() {

  }
  update() {
    
  }
  command() {
    
  }
}
module.exports = {
  Plugin:MyPlugin
}
```

### setup

* obligatory

should return the plugins schema either asynchronous as promise resolve or by synchronous return.

In any case, a return has to happen. If there is data that needs to be loaded before any session start, it's important to return via Promise. So once the plugin uses listeners, it needs to wait until all collectItems functions returned. Otherwise any listener that is restored on startup has trouble finding involved items.

Check the [plugin module documentation](https://plaitools.gitlab.io/documentation/plaiframe/plugins/module-plugin-Plugin.html#setup) for arguments handed to the setup function by the session.

### update

* optional

is called whenever changes are made to the plugin settings.

Check the [plugin module documentation](https://plaitools.gitlab.io/documentation/plaiframe/plugins/module-plugin-Plugin.html#update) for arguments handed to the update function by the game.


### cue

* obligatory

session forwards all cue data, that is related to plugin as defined in its schema.

Cue may return the following data types:

#### return types

* undefined

session will ignore the returned or resolved value

* string

a string resolved or returned, immediately dispatches a followup cue with name being the returned string. If other cue() function calls to this or another plugin return or resolve a string, the session dispatches the followup that was returned by the upmost property in the cue

* function

If a listener is created, cue should return or resolve a cancel function, that allows the session to cancel the listener.

#### parameters

Check the [plugin module documentation](https://plaitools.gitlab.io/documentation/plaiframe/plugins/module-plugin-Plugin.html#cue) for all arguments handed to the cue function by the session.

### command

* optional

Basic functions of the plaiframe can be controlled via command line. Commands are handed to plugins if they are addressed by name. Plaiframe will call the `command()` function with whatever is typed after the plugin name.

Input is split at each space characters.

``` javascript
command(Input) {
  switch(input[0]) {
    case "mycommand":
      log.info("myplugin","do mycommand considering this: " + input[1] + " and that: " + input[2])
      break
  }
}
```
If you use the `Plugin` class I recommend to default input to the parent command function. It will check if input matches a plugin collection and forward the command.

``` javascript
command(Input) {
  switch(input[0]) {
    case "mycommand":
      log.info("myplugin","do mycommand considering this: " + input[1] + " and that: " + input[2])
      break
    default:
      super.command(input)
  }
}
```

Events
-------

`setup()` and `cue()` function arguments both provide an EventEmitter instance. The setup event emitter in the config argument allows to dispatch game events. The cue event emitter allows to dispatch session events.

If events are specified in the plugin schema, they can be used in the editor.

Use the game or session object to dispatch (emit) or listen on events.

``` javascript
class MyPlugin {
  constructor() {
    
  }
  setup(config) {
    // Emit an event, that can be listened on in all active sessions
    config.game.event.emit("myEvent", "foo")
  }

  cue(data, session) {
    // Emit an event, that can only be listened on in the session the cue originates in.
    session.event.emit("anotherEvent", {foo:"hello"})
  }
}
module.exports.Plugin = MyPlugin
```


License and Copyright
---------------------

Copyright 2021 machina eX

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
