diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/3dpentutorial/index.html b/3dpentutorial/index.html new file mode 100644 index 000000000..bc9cdb212 --- /dev/null +++ b/3dpentutorial/index.html @@ -0,0 +1,588 @@ + + + + + + + + Creating a 3D Pen - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Creating a 3D Pen for Ubiq

+

In this short tutorial we'll make a pen that lets us draw shapes in mid-air. We'll add simple networking with Ubiq so you can share your drawings with others!

+

0) Download and install the Unity editor. We are using Unity 2020.3.40, but later versions should also work.

+

1) Create a new Unity project with the 3D template.

+

2) Download the Ubiq package for Unity (v0.3.0): release page, direct file link

+

3) Extract the Ubiq package into your new project's Packages folder. You should have the file structure: Packages/ubiq-0.3.0/Editor, as in the image below.

+

+

4) Open or return focus to your Unity editor and wait for Ubiq to be imported.

+

5) Open the Unity package manager with the top menu. The path is Window/Package Manager, highlighted in the image below.

+

+

6) In the package manager, select Ubiq from the list on the left. In the pane on the right, click to expand the Samples dropdown, then Import to load the Ubiq samples. Wait for the import to complete.

+

+

7) Open the Ubiq intro scene from the Samples: Assets/Samples/Ubiq/0.3.0/Samples/Start Here

+

8) Now we have everything we need, let's make a simple object for the pen. This can be anything you like! We will make a simple stand-in with one big cylinder for the grip and a tiny one for the nib. Right click in the hierarchy window and select Create Empty. Right click on the object in the hierarchy, select Rename, and give it the name "3DPen". This will be our parent object. We can use this to customize how our object is picked up. Now right click on 3DPen in the Hierarchy window and select 3D Object/Cylinder. Rename this cylinder "Grip". Right click on Grip and again select 3D Object/Cylinder. Rename this new cylinder "Nib". Your hierarchy should now read 3DPen/Grip/Nib, as in the image.

+

+

9) Scale, and translate the objects so they (sort of!) resemble a pen. Do not translate the top object (3DPen), as this will get moved when the user grabs it. Focus on just moving and scaling Grip and Nib. Do not worry about rotation now; we'll deal with this later. Tip: the scale of the sample is 1m equals 1 unit. Finally, position the pen somewhere you can easily reach it. You could try next to the menu. Here's what we ended up with:

+

+

10) Select 3DPen in the hierarchy window. Now, in the Inspector window, select Add Component and add a Rigidbody. In the Rigidbody, enable Is Kinematic.

+

+

11) Now let's write a script to help us pick up the pen and move it around. Select Add Component again, and type Pen. Unity will prompt you to add a new script with that name - select New Script, then Create and Add.

+

+

12) This will create the Pen script in your Assets folder and attach that script to the object. Open the file (Assets/Pen.cs) and replace its contents with the following:

+
using UnityEngine;
+using Ubiq.XR;
+
+// Implement Graspable interface, part of Ubiq XR interaction
+// You can use any interaction toolkit you like with Ubiq!
+// For the sake of keeping this tutorial simple, we use our simple in-built
+// option.
+public class Pen : MonoBehaviour, IGraspable
+{
+    private Hand controller;
+
+    private void LateUpdate()
+    {
+        if (controller)
+        {
+            transform.position = controller.transform.position;
+            transform.rotation = controller.transform.rotation;
+        }
+    }
+
+    void IGraspable.Grasp(Hand controller)
+    {
+        this.controller = controller;
+    }
+
+    void IGraspable.Release(Hand controller)
+    {
+        this.controller = null;
+    }
+}
+
+

This implements the Graspable interface provided by Ubiq's XR interaction tools. You can use any interaction toolkit you like with Ubiq, but for the purpose of keeping this tutorial simple, we use our simple in-built option.

+

12) Enter Play mode by pressing the arrow at the top of the Unity Editor, or with the shortcut Ctrl-P. You can use Ubiq's desktop controls to walk over to the pen (WASD), look at it (hold right-click while moving the mouse), and grasp it (middle-mouse-button while the mouse is over the pen). When you move your view (as before, hold right-click while moving the mouse) again, the object should move with it.

+

13) Let's also build your application to test networking functionality. First, go to the top bar, then Edit/Project Settings. In the Project Settings window, select Player from the list on the left. In the pane on the right, click the dropdown next to Fullscreen Mode and select Windowed, then set the window to something small, like 640 x 480. This helps us test because we can see both the editor and the application running in a small window. Now, again in the top bar, go to File/Build and Run. Select a location for the build, and wait for the build to complete.

+

14) Now your application should be running in both the editor and as a windowed standalone app. To connect the two, we'll need them to both join the same room. On the editor, use your mouse to click the New button on the Ubiq UI panel in the scene. Leave the name as default, and click the arrow at the top right of the UI panel. Finally, select "No, keep my room private". The panel will change to show you a three letter code. This is the 'joincode' for your room. In your standalone windowed application, click Join on the Ubiq sample UI, enter this code, then click the arrow to submit.

+

15) Now you have two applications, both connected to the same room. On both, you should now see another avatar in the room with you. Try picking up the object as in Step 12. You'll see that it moves for the user who picked it up, but in the other application it stays still. This means we need to add some networking!

+

16) Replace your Pen script (Assets/Pen.cs) with the following:

+
using UnityEngine;
+using Ubiq.XR;
+using Ubiq.Messaging; // new
+
+public class Pen : MonoBehaviour, IGraspable
+{
+    private NetworkContext context; // new
+    private bool owner; // new
+    private Hand controller;
+
+    // new
+    // 1. Define a message format. Let's us know what to expect on send and recv
+    private struct Message
+    {
+        public Vector3 position;
+        public Quaternion rotation;
+
+        public Message(Transform transform)
+        {
+            this.position = transform.position;
+            this.rotation = transform.rotation;
+        }
+    }
+
+    // new
+    private void Start()
+    {
+        // 2. Register the object with the network scene. This provides a
+        // NetworkID for the object and lets it get messages from remote users
+        context = NetworkScene.Register(this);
+    }
+
+    // new
+    public void ProcessMessage (ReferenceCountedSceneGraphMessage msg)
+    {
+        // 3. Receive and use transform update messages from remote users
+        // Here we use them to update our current position
+        var data = msg.FromJson<Message>();
+        transform.position = data.position;
+        transform.rotation = data.rotation;
+    }
+
+    // new
+    private void FixedUpdate()
+    {
+        if (owner)
+        {
+            // 4. Send transform update messages if we are the current 'owner'
+            context.SendJson(new Message(transform));
+        }
+    }
+
+    private void LateUpdate()
+    {
+        if (controller)
+        {
+            transform.position = controller.transform.position;
+            transform.rotation = controller.transform.rotation;
+        }
+    }
+
+    void IGraspable.Grasp(Hand controller)
+    {
+        // 5. Define ownership as 'who holds the item currently'
+        owner = true; // new
+        this.controller = controller;
+    }
+
+    void IGraspable.Release(Hand controller)
+    {
+        // As 5. above, define ownership as 'who holds the item currently'
+        owner = false; // new
+        this.controller = null;
+    }
+
+     // Note about ownership: 'ownership' is just one way of designing this
+     // kind of script. It's sometimes a useful pattern, but has no special
+     // significance outside of this file or in Ubiq more generally.
+}
+
+

New lines are functioned are marked with a comment. This script does a number of important things, marked in the code.

+

17) Test again as described in Steps 12-14. You should now see that when the object is grasped and moved in one application, it also moves in the other!

+

18) Now let's get the pen to draw in 3D space! Replace Pen.cs with the following:

+
using UnityEngine;
+using Ubiq.XR;
+using Ubiq.Messaging;
+
+public class Pen : MonoBehaviour, IGraspable, IUseable // new
+{
+    private NetworkContext context;
+    private bool owner;
+    private Hand controller;
+    private Transform nib; // new
+    private Material drawingMaterial; // new
+    private GameObject currentDrawing; // new
+
+    private struct Message
+    {
+        public Vector3 position;
+        public Quaternion rotation;
+
+        public Message(Transform transform)
+        {
+            this.position = transform.position;
+            this.rotation = transform.rotation;
+        }
+    }
+
+    private void Start()
+    {
+        nib = transform.Find("Grip/Nib"); // new
+        context = NetworkScene.Register(this);
+        var shader = Shader.Find("Particles/Standard Unlit"); // new
+        drawingMaterial = new Material(shader); // new
+    }
+
+    public void ProcessMessage (ReferenceCountedSceneGraphMessage msg)
+    {
+        var data = msg.FromJson<Message>();
+        transform.position = data.position;
+        transform.rotation = data.rotation;
+    }
+
+    private void FixedUpdate()
+    {
+        if (owner)
+        {
+            context.SendJson(new Message(transform));
+        }
+    }
+
+    private void LateUpdate()
+    {
+        if (controller)
+        {
+            transform.position = controller.transform.position;
+            transform.rotation = controller.transform.rotation;
+        }
+    }
+
+    void IGraspable.Grasp(Hand controller)
+    {
+        owner = true;
+        this.controller = controller;
+    }
+
+    void IGraspable.Release(Hand controller)
+    {
+        owner = false;
+        this.controller = null;
+    }
+
+    // new
+    void IUseable.Use(Hand controller)
+    {
+        BeginDrawing();
+    }
+
+    // new
+    void IUseable.UnUse(Hand controller)
+    {
+        EndDrawing();
+    }
+
+    // new
+    private void BeginDrawing()
+    {
+        currentDrawing = new GameObject("Drawing");
+        var trail = currentDrawing.AddComponent<TrailRenderer>();
+        trail.time = Mathf.Infinity;
+        trail.material = drawingMaterial;
+        trail.startWidth = .05f;
+        trail.endWidth = .05f;
+        trail.minVertexDistance = .02f;
+
+        currentDrawing.transform.parent = nib.transform;
+        currentDrawing.transform.localPosition = Vector3.zero;
+        currentDrawing.transform.localRotation = Quaternion.identity;
+    }
+
+    // new
+    private void EndDrawing()
+    {
+        var trail = currentDrawing.GetComponent<TrailRenderer>();
+        currentDrawing.transform.parent = null;
+        currentDrawing.GetComponent<TrailRenderer>().emitting = false;
+        currentDrawing = null;
+    }
+}
+
+

19) Test as in steps 12-14. You should now be able to draw a line in the air with the pen! This is intuitive in virtual reality - pick up the item and grasp buttons/triggers, and use with main button/trigger. The desktop interface is fiddly for this, but okay for debug: First, click on the pen to 'use' it - you should see a debug message in the Unity editor if successful. Then, while still holding left mouse to use, hold right mouse to move your view around. But you'll notice that the line is only drawn locally so far - the remote user does not yet see it. We'll change that in the next step.

+

20) Time to add networking to our drawings! Replace Pen.cs with this final version:

+
using UnityEngine;
+using Ubiq.XR;
+using Ubiq.Messaging;
+
+// Adds simple networking to the 3d pen. The approach used is to draw locally
+// when a remote user tells us they are drawing, and stop drawing locally when
+// a remote user tells us they are not.
+public class Pen : MonoBehaviour, IGraspable, IUseable
+{
+    private NetworkContext context;
+    private bool owner;
+    private Hand controller;
+    private Transform nib;
+    private Material drawingMaterial;
+    private GameObject currentDrawing;
+
+    // Amend message to also store current drawing state
+    private struct Message
+    {
+        public Vector3 position;
+        public Quaternion rotation;
+        public bool isDrawing; // new
+
+        public Message(Transform transform, bool isDrawing)
+        {
+            this.position = transform.position;
+            this.rotation = transform.rotation;
+            this.isDrawing = isDrawing; // new
+        }
+    }
+
+    private void Start()
+    {
+        nib = transform.Find("Grip/Nib");
+        context = NetworkScene.Register(this);
+        var shader = Shader.Find("Particles/Standard Unlit");
+        drawingMaterial = new Material(shader);
+    }
+
+    public void ProcessMessage (ReferenceCountedSceneGraphMessage msg)
+    {
+        var data = msg.FromJson<Message>();
+        transform.position = data.position;
+        transform.rotation = data.rotation;
+
+        // new
+        // Also start drawing locally when a remote user starts
+        if (data.isDrawing && !currentDrawing)
+        {
+            BeginDrawing();
+        }
+        if (!data.isDrawing && currentDrawing)
+        {
+            EndDrawing();
+        }
+    }
+
+    private void FixedUpdate()
+    {
+        if (owner)
+        {
+            // new
+            context.SendJson(new Message(transform,isDrawing:currentDrawing));
+        }
+    }
+
+    private void LateUpdate()
+    {
+        if (controller)
+        {
+            transform.position = controller.transform.position;
+            transform.rotation = controller.transform.rotation;
+        }
+    }
+
+    void IGraspable.Grasp(Hand controller)
+    {
+        owner = true;
+        this.controller = controller;
+    }
+
+    void IGraspable.Release(Hand controller)
+    {
+        owner = false;
+        this.controller = null;
+    }
+
+    void IUseable.Use(Hand controller)
+    {
+        BeginDrawing();
+    }
+
+    void IUseable.UnUse(Hand controller)
+    {
+        EndDrawing();
+    }
+
+    private void BeginDrawing()
+    {
+        currentDrawing = new GameObject("Drawing");
+        var trail = currentDrawing.AddComponent<TrailRenderer>();
+        trail.time = Mathf.Infinity;
+        trail.material = drawingMaterial;
+        trail.startWidth = .05f;
+        trail.endWidth = .05f;
+        trail.minVertexDistance = .02f;
+
+        currentDrawing.transform.parent = nib.transform;
+        currentDrawing.transform.localPosition = Vector3.zero;
+        currentDrawing.transform.localRotation = Quaternion.identity;
+    }
+
+    private void EndDrawing()
+    {
+        var trail = currentDrawing.GetComponent<TrailRenderer>();
+        currentDrawing.transform.parent = null;
+        currentDrawing.GetComponent<TrailRenderer>().emitting = false;
+        currentDrawing = null;
+    }
+}
+
+

And we're done! Test it again as with steps 12-14, and if you have a headset available, see how it feels in VR!

+

You might notice that drawings are not visible to new joining users. A more advanced implementation would store the points of the drawings in Peer or Room properties, so new users could see them when they join. If you do try it, let us know how you get on!

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/404.html b/404.html new file mode 100644 index 000000000..7408da053 --- /dev/null +++ b/404.html @@ -0,0 +1,200 @@ + + + + + + + + Ubiq Docs + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • +
  • +
  • +
+
+
+
+
+ + +

404

+ +

Page not found

+ + +
+
+ +
+
+ +
+ +
+ +
+ + + + + +
+ + + + + + + + + diff --git a/asyncdesignpatterns/index.html b/asyncdesignpatterns/index.html new file mode 100644 index 000000000..5a540419a --- /dev/null +++ b/asyncdesignpatterns/index.html @@ -0,0 +1,286 @@ + + + + + + + + Async Design Patterns - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Asyncrhonous Design Patterns in Unity

+

The Unity process manages the main thread, which begins before any user code is executed. Most Unity resources can only be accessed from the main thread; an exception will be thrown otherwise. +There are still many possibilities for writing aysnchronous code however.

+

Design Pattern

+

Delayed initialisation with callbacks. Mimics the do-then pattern in JS. Methods are called which take Actions. Those Actions are initialised by lambdas. The lambas execution thread depends on the called function.

+
void Start()
+{
+    factory.GetRtcConfiguration(config =>
+    {
+        pc = factory.CreatePeerConnection(config, this);
+    });           
+}
+
+

Design Pattern

+

Message pumps with Update. Commonly used in the mid-level networking code, this pattern uses a list of actions to execute methods on the main thread.

+
class RoomClient
+{
+    private List<Action> actions = new List<Action>();
+
+    public void SendToServer(Message message)
+    {
+        actions.Add(() =>
+        {
+            SendToServerSync(message);
+        });
+    }
+
+    private void Update()
+    {
+        foreach (var action in actions)
+        {
+            action();
+        }
+        actions.Clear();
+    }
+}
+
+

Design Pattern

+

Commonly used in webrtc code for objects that take time to initialise because they are waiting on external resources. This pattern uses coroutines to effectively poll a resource, conditionally executing operations on the main thread.

+
    void Start()
+    {
+        factory.GetRtcConfiguration(config =>
+        {
+            pc = factory.CreatePeerConnection(config, this);
+        });           
+    }
+
+    private IEnumerator WaitForPeerConnection(Action OnPcCreated)
+    {
+        while (pc == null)
+        {
+            yield return null;
+        }
+        OnPcCreated();
+    }
+
+    public void AddLocalAudioSource()
+    {
+        StartCoroutine(WaitForPeerConnection(() =>
+        {
+            var audiosource = factory.CreateAudioSource();
+            var audiotrack = factory.CreateAudioTrack("localAudioSource", audiosource);
+            pc.AddTrack(audiotrack, new[] { "localAudioSource" });
+        }));
+    }
+
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/botssample/index.html b/botssample/index.html new file mode 100644 index 000000000..8ff5938c3 --- /dev/null +++ b/botssample/index.html @@ -0,0 +1,311 @@ + + + + + + + + Bots - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Bots

+

Ubiq has the ability to simulate large numbers of users. This may be useful when stress testing, for example. This is demonstrated in the Bots Application Sample.

+

Bot Peer

+

In a regular application, a user interacts with the world through the PlayerController. They connect to the network using a NetworkScene and become a Ubiq Peer.

+

It is the same with the Bot and Bot Peer. The Bot GameObject contains Components which interact with the world. Pairing a Bot with a NetworkScene creates a Bot Peer, that can connect to a room and act autonomously.

+

+

The Bot Peer is equivalent to a user application. In the Editor, the Bot Peer can be joined to a room by using the RoomClient Editor Controls, just like a regular Peer. Alternatively, the default Bot, and others, can be controlled en-masse using the Bots Controller.

+

+

The Bot Peer Prefab can be dropped into new scenes and controlled via the Editor, where small numbers of Bots are required.

+

Bot

+

The Bot Prefab implements the behaviour of the non-player controlled character. The sample Bot contains a number of common abilities, which can be extended by adding additional custom Components.

+

Avatar

+

The Bot Peer contains a skeleton allowing it to embody an Avatar at remote peers.

+

Behaviour

+

The Bot Peer Avatar is controlled by the Bot Component. This uses a nav mesh to move between random points in the environment.

+

Audio

+

The Bot Peer can speak with a pre-recorded audio clip. This is through a Component that takes the place of the Microphone input in a player-controlled Peer.

+

Managing Multiple Bots

+

Usually, a single Unity process is a single Ubiq Peer, with one NetworkScene. This is because a single PC can usually only drive one set of user input devices at a time.

+

With bots, a single process can host multiple Bots, but each Bot is still a separate Peer, and has its own NetworkScene. This is achived using the same scene-graph Forests as the Local Loopback scene.

+

+

BotsManager & BotsController

+

The Bots Application Sample contains Components to create and manage large numbers of bots.

+

The BotsManager is the Component that creates and configures Bot Peers within a single process. Bots Managers are controlled remotely by a BotsController.

+

+

The BotsController and BotsManager use Ubiq to communicate. The BotsManager and BotsController join the same Command and Control Room. The Bot Peers themselves join different rooms, possibly even on different servers. The BotManager communicates with the Bot Peers through the local scene graph.

+

Multiple Bot Managers across different processes can be controlled this way, each managing a number of Bots.

+

+

A typical Unity process running on a modern desktop can handle approximately 20 bots. All three Components can run within one scene graph, within one process, as in the Bots Sample Scene, or can be split between multiple machines to control hundreds of bots.

+

Creating Bots with the Bots Scene

+

Opening the Bots scene and pressing Play will show a birds-eye view of the local process, and a control panel. This scene is only meant to be used on the desktop.

+

+

The Bots scene already has one Bot Peer created. This GameObject's NetworkScene Editor Controls or the Create Room/Join Room Control Panel Buttons can be used to immediately join the Bot into a room with other Peers - bots or normal Peers.

+

Stress Testing with the Bots scene

+

One of the uses for Bots is stress testing. The Bots Sample is set up to show this.

+

The Bots Scene contains a number of Components, and a UI.

+

Bots Control

+

This GameObject contains resources to allow a user to control one or more Bot Managers with the UI. It also includes the Camera and Event Manager. The UI contained under Bots Control drives the BotsController, which in turn directs Bot Managers.

+

The BotsController is in the Control Room GameObject, which has its own NetworkScene and so forms the Bots Controller Peer.

+

This branch also contains a Peer (Bots Room Peer) that can be connected to the Bots' room, in order to do things such as take statistics or collect logs.

+

Bots Manager

+

The Bots Manager GameObject is the forest for the Bots Manager Peer. Though they are in the same scene graph, the Bots Manager Peer and Bots Controller Peer both join the same command and control room.

+

Environment

+

To reduce memory overhead, multiple Bot Peers share the same Environment, though as they have their own NetworkScene they don't directly interact outside of Ubiq networking.

+

Bot Peer 1

+

The Scene includes one Bot Peer instance. The Control Panel can be used to add more.

+

Bots Config

+

The Bots Config GameObject hosts a helper Component to set which servers the Control Room and Bots Room should be hosted on at design time. By allowing these to be set at design time, the Bots Scene can be built to an executable to be run headless on different machines without needing further configuration.

+

Make sure to change the Control Room Id before running the sample against Nexus, in case others are running the same sample.

+

Controlling Bots

+

The Bots Manager is used to spawn new Bot Peer instances. Bot Peers exist in the same application but are completley independent as far as the network, and other peers, are concerned. +The UI in the Bots scene is for the Bot Controller. There is no interface for interacting with the virtual world, VR or otherwise, as there is no Network Scene for players in the Bots example. +Bots can be instructed to join any room however, including those with regular players.

+

Command and Control and Bots Rooms

+

When the Scene is started, the Control Panel UI will control the local Bot Manager. However, there is a limit to how many Bot Peers a single Unity process can host.

+

Bot Managers across different Unity processes can work together to control very large numbers of bots.

+

The Control Panel and Bot Manager are two distinct Ubiq Peers. They communicate using Ubiq Messages through a 'Command and Control' Room.

+

When the Control Panel starts, it creates a new Room and has the local Bot Manager join it. Additional Bot Managers from other processes can join this room too, and fall under the control of the Control Panel.

+

The Command and Control Room can be any Ubiq Room, including the one that the Bots join.

+

The Room can be set via the command line with the -commandroomjoincode argument. This, combined with -batchmode, can allow many headless Unity instances to spawn bots.

+

In practice though, when doing things like stress testing, the Room should be different, even on a different server if doing server stress testing.

+

Servers

+

There are in total four distinct Peers in the single Bots Sample: the Bot Controller (Control Room), the Bot Manager (Control Room), the Bot Room Peer (Bots Room) and the default Bot Peer (Bots Room).

+

The Bots Config Component is used to change the server(s) for all of these at once.

+

The default server for Bot Peer 1 is set in the standard NetworkScene Prefab; it is overridden before connecting when the Control Panel is used to control the Bots.

+

Control Panel

+

The UI has two sets of controls: Common controls and Instance controls.

+

Common Controls apply to all Bot Managers, and Instance controls apply to just that Bot Manager.

+

When only one Bot Manager instance is known, the controls behave identically.

+

Toggle Camera

+

As each Peer acts as if it were the only one in the process, each Peer will show all other Peers Avatars. This can create high rendering loads. Avatars can be hidden by toggling the Camera, which changes the Culling Mask. The camera is not completley disabled, as it is needed for the UI.

+

Enable Audio

+

Enables or Disables the audio chat channel for new Bots. This can be used to reduce compute load on the clients. This may be desireable if stress testing a server, for example, as audio data doesn't pass through the server.

+

Create/Join Room

+

The Create Room button creates a new room for Bots, and commands any existing Bots to join it. Alternatively, an existing Join Code can be entered and the Join Room button used. Either can be used as many times as desired to change all the Bots at once, without re-creating the Bots.

+

Number of Peers

+

Shows the number of Peers in the Bots Room, including the 'dummy' peer. A process may host a number of Bots that have not yet joined a room. Use this figure to monitor the actual number of bots in a room together.

+

Bot Manager Instances

+

Below the Common Controls each Bot Manager that the Controller is aware of is listed. Each line shows the Id of the Manager, as well as a the number of bots it is hosting. The Input Field and Add Bot Button can be used add new Bots to that instance.

+

If a Bots Room has been set up, new Bots will automatically join it.

+

The FPS is used to approximate the performance or load of the Bots Manager. The Colour is controlled by the Fps Gradient member of the Bots Manager Control Prefab (between 0-100 Fps).

+

Bot Scene

+

The Bots Application Sample also contains a sample scene with a single Bot Peer, along with a Player Peer. This can be used to see what it is like for players to interact with Bots. In this scene, the Player and the Bot should be joined to the same room manually using the RoomClient Editor controls.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/creatinganetworkedobject/index.html b/creatinganetworkedobject/index.html new file mode 100644 index 000000000..911f7de42 --- /dev/null +++ b/creatinganetworkedobject/index.html @@ -0,0 +1,441 @@ + + + + + + + + Creating Networked Objects - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Building a Basic Networked Object

+

Networked objects are Components that can keep themselves synchronised by exchanging messages over the network. You can create new Networked Objects to implement your own networked behaviour.

+

1) Create a new Unity Script and add it to the GameObject that you want to be networked. You can do this via the inspector by clicking on "Add Component" and typing the new name.

+

+

2) Include Ubiq.Messaging

+
    using System.Collections;
+    using System.Collections.Generic;
+    using UnityEngine;
+    using Ubiq.Messaging;
+
+    public class MyNetworkedObject : MonoBehaviour
+    {
+        // Start is called before the first frame update
+        void Start()
+        {
+        }
+
+        // Update is called once per frame
+        void Update()
+        {
+
+        }
+    }
+
+

3) Create a new member, context. context will hold the address of your object on the network, and allow you to send messages.

+
using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Ubiq.Messaging;
+
+public class MyNetworkedObject : MonoBehaviour
+{
+    NetworkContext context;
+
+    // Start is called before the first frame update
+    void Start()
+    {
+
+    }
+
+    // Update is called once per frame
+    void Update()
+    {
+
+    }
+}
+
+

4) Declare a method called ProcessMessage, which takes a ReferenceCountedSceneGraphMessage. This is where messages to your Component will come in.

+
using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Ubiq.Messaging;
+
+public class MyNetworkedObject : MonoBehaviour
+{
+    NetworkContext context;
+
+    // Start is called before the first frame update
+    void Start()
+    {
+
+    }
+
+    // Update is called once per frame
+    void Update()
+    {
+
+    }
+
+    public void ProcessMessage(ReferenceCountedSceneGraphMessage message)
+    {
+
+    }
+}
+
+

5) In your Start() method, call NetworkScene.Register(). This registers your Component with Ubiq and gets it an address on the network. The return value is a NetworkContext which you can store in the member created previously.

+
using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Ubiq.Messaging;
+
+public class MyNetworkedObject : MonoBehaviour
+{
+    NetworkContext context;
+
+    // Start is called before the first frame update
+    void Start()
+    {
+        context = NetworkScene.Register(this);
+    }
+
+    // Update is called once per frame
+    void Update()
+    {
+
+    }
+
+    public void ProcessMessage(ReferenceCountedSceneGraphMessage message)
+    {
+
+    }
+}
+
+

6) Define what a message between instances of your Component will look like. In the message, write the variables that you want to send. Below we create a message to send the object's position.

+
using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Ubiq.Messaging;
+
+public class MyNetworkedObject : MonoBehaviour
+{
+    NetworkContext context;
+
+    // Start is called before the first frame update
+    void Start()
+    {
+        context = NetworkScene.Register(this);
+    }
+
+    // Update is called once per frame
+    void Update()
+    {
+
+    }
+
+    private struct Message
+    {
+        public Vector3 position;
+    }
+
+    public void ProcessMessage(ReferenceCountedSceneGraphMessage message)
+    {
+
+    }
+}
+
+

7) Add code to parse and process incoming messages to ProcessMessage. Below, we convert the ReferenceCountedSceneGraphMessage into a Message, and then access the position member to set the object's position in world space.

+
using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Ubiq.Messaging;
+
+public class MyNetworkedObject : MonoBehaviour
+{
+    NetworkContext context;
+
+    // Start is called before the first frame update
+    void Start()
+    {
+        context = NetworkScene.Register(this);
+    }
+
+    // Update is called once per frame
+    void Update()
+    {
+
+    }
+
+    private struct Message
+    {
+        public Vector3 position;
+    }
+
+    public void ProcessMessage(ReferenceCountedSceneGraphMessage message)
+    {
+        // Parse the message
+        var m = message.FromJson<Message>();
+
+        // Use the message to update the Component
+        transform.localPosition = m.position;
+    }
+}
+
+

8) Messages will only be sent to your Component, from other instances of your Component, so you also need to Send messages as well. This is done through the NetworkContext you recieved when the Component was registered.

+

Below, we check if the position of the object has changed in the last frame, and if so, send the new position to all other instances of the object. +We detect if the position has changed by keeping track of the position in the last frame in a new member, lastPosition.

+

We also modify ProcessMessage slightly, to update lastPosition when a message is received - otherwise, an incoming message will generate an outgoing message, and two Components will send messages back and forth in an endless cycle even if the player hasn't changed the objects position!

+
using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using Ubiq.Messaging;
+
+public class MyNetworkedObject : MonoBehaviour
+{
+    NetworkContext context;
+
+    // Start is called before the first frame update
+    void Start()
+    {
+        context = NetworkScene.Register(this);
+    }
+
+    Vector3 lastPosition;
+
+    // Update is called once per frame
+    void Update()
+    {
+        if(lastPosition != transform.localPosition)
+        {
+            lastPosition = transform.localPosition;
+            context.SendJson(new Message()
+            {
+                position = transform.localPosition
+            });
+        }
+    }
+
+    private struct Message
+    {
+        public Vector3 position;
+    }
+
+    public void ProcessMessage(ReferenceCountedSceneGraphMessage message)
+    {
+        // Parse the message
+        var m = message.FromJson<Message>();
+
+        // Use the message to update the Component
+        transform.localPosition = m.position;
+
+        // Make sure the logic in Update doesn't trigger as a result of this message
+        lastPosition = transform.localPosition;
+    }
+}
+
+

9) Your first networked object is now complete!

+

Add a cube to your object so you can see it in the scene. Continue with the tutorials to see it in action!

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/css/fonts/Roboto-Slab-Bold.woff b/css/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 000000000..6cb600001 Binary files /dev/null and b/css/fonts/Roboto-Slab-Bold.woff differ diff --git a/css/fonts/Roboto-Slab-Bold.woff2 b/css/fonts/Roboto-Slab-Bold.woff2 new file mode 100644 index 000000000..7059e2314 Binary files /dev/null and b/css/fonts/Roboto-Slab-Bold.woff2 differ diff --git a/css/fonts/Roboto-Slab-Regular.woff b/css/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 000000000..f815f63f9 Binary files /dev/null and b/css/fonts/Roboto-Slab-Regular.woff differ diff --git a/css/fonts/Roboto-Slab-Regular.woff2 b/css/fonts/Roboto-Slab-Regular.woff2 new file mode 100644 index 000000000..f2c76e5bd Binary files /dev/null and b/css/fonts/Roboto-Slab-Regular.woff2 differ diff --git a/css/fonts/fontawesome-webfont.eot b/css/fonts/fontawesome-webfont.eot new file mode 100644 index 000000000..e9f60ca95 Binary files /dev/null and b/css/fonts/fontawesome-webfont.eot differ diff --git a/css/fonts/fontawesome-webfont.svg b/css/fonts/fontawesome-webfont.svg new file mode 100644 index 000000000..855c845e5 --- /dev/null +++ b/css/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/css/fonts/fontawesome-webfont.ttf b/css/fonts/fontawesome-webfont.ttf new file mode 100644 index 000000000..35acda2fa Binary files /dev/null and b/css/fonts/fontawesome-webfont.ttf differ diff --git a/css/fonts/fontawesome-webfont.woff b/css/fonts/fontawesome-webfont.woff new file mode 100644 index 000000000..400014a4b Binary files /dev/null and b/css/fonts/fontawesome-webfont.woff differ diff --git a/css/fonts/fontawesome-webfont.woff2 b/css/fonts/fontawesome-webfont.woff2 new file mode 100644 index 000000000..4d13fc604 Binary files /dev/null and b/css/fonts/fontawesome-webfont.woff2 differ diff --git a/css/fonts/lato-bold-italic.woff b/css/fonts/lato-bold-italic.woff new file mode 100644 index 000000000..88ad05b9f Binary files /dev/null and b/css/fonts/lato-bold-italic.woff differ diff --git a/css/fonts/lato-bold-italic.woff2 b/css/fonts/lato-bold-italic.woff2 new file mode 100644 index 000000000..c4e3d804b Binary files /dev/null and b/css/fonts/lato-bold-italic.woff2 differ diff --git a/css/fonts/lato-bold.woff b/css/fonts/lato-bold.woff new file mode 100644 index 000000000..c6dff51f0 Binary files /dev/null and b/css/fonts/lato-bold.woff differ diff --git a/css/fonts/lato-bold.woff2 b/css/fonts/lato-bold.woff2 new file mode 100644 index 000000000..bb195043c Binary files /dev/null and b/css/fonts/lato-bold.woff2 differ diff --git a/css/fonts/lato-normal-italic.woff b/css/fonts/lato-normal-italic.woff new file mode 100644 index 000000000..76114bc03 Binary files /dev/null and b/css/fonts/lato-normal-italic.woff differ diff --git a/css/fonts/lato-normal-italic.woff2 b/css/fonts/lato-normal-italic.woff2 new file mode 100644 index 000000000..3404f37e2 Binary files /dev/null and b/css/fonts/lato-normal-italic.woff2 differ diff --git a/css/fonts/lato-normal.woff b/css/fonts/lato-normal.woff new file mode 100644 index 000000000..ae1307ff5 Binary files /dev/null and b/css/fonts/lato-normal.woff differ diff --git a/css/fonts/lato-normal.woff2 b/css/fonts/lato-normal.woff2 new file mode 100644 index 000000000..3bf984332 Binary files /dev/null and b/css/fonts/lato-normal.woff2 differ diff --git a/css/theme.css b/css/theme.css new file mode 100644 index 000000000..ad773009b --- /dev/null +++ b/css/theme.css @@ -0,0 +1,13 @@ +/* + * This file is copied from the upstream ReadTheDocs Sphinx + * theme. To aid upgradability this file should *not* be edited. + * modifications we need should be included in theme_extra.css. + * + * https://github.com/readthedocs/sphinx_rtd_theme + */ + + /* sphinx_rtd_theme version 1.2.0 | MIT license */ +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search>a:hover{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.version{margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel{border:1px solid #7fbbe3;background:#e7f2fa;font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} diff --git a/css/theme_extra.css b/css/theme_extra.css new file mode 100644 index 000000000..ab0631a18 --- /dev/null +++ b/css/theme_extra.css @@ -0,0 +1,197 @@ +/* + * Wrap inline code samples otherwise they shoot of the side and + * can't be read at all. + * + * https://github.com/mkdocs/mkdocs/issues/313 + * https://github.com/mkdocs/mkdocs/issues/233 + * https://github.com/mkdocs/mkdocs/issues/834 + */ +.rst-content code { + white-space: pre-wrap; + word-wrap: break-word; + padding: 2px 5px; +} + +/** + * Make code blocks display as blocks and give them the appropriate + * font size and padding. + * + * https://github.com/mkdocs/mkdocs/issues/855 + * https://github.com/mkdocs/mkdocs/issues/834 + * https://github.com/mkdocs/mkdocs/issues/233 + */ +.rst-content pre code { + white-space: pre; + word-wrap: normal; + display: block; + padding: 12px; + font-size: 12px; +} + +/** + * Fix code colors + * + * https://github.com/mkdocs/mkdocs/issues/2027 + */ +.rst-content code { + color: #E74C3C; +} + +.rst-content pre code { + color: #000; + background: #f8f8f8; +} + +/* + * Fix link colors when the link text is inline code. + * + * https://github.com/mkdocs/mkdocs/issues/718 + */ +a code { + color: #2980B9; +} +a:hover code { + color: #3091d1; +} +a:visited code { + color: #9B59B6; +} + +/* + * The CSS classes from highlight.js seem to clash with the + * ReadTheDocs theme causing some code to be incorrectly made + * bold and italic. + * + * https://github.com/mkdocs/mkdocs/issues/411 + */ +pre .cs, pre .c { + font-weight: inherit; + font-style: inherit; +} + +/* + * Fix some issues with the theme and non-highlighted code + * samples. Without and highlighting styles attached the + * formatting is broken. + * + * https://github.com/mkdocs/mkdocs/issues/319 + */ +.rst-content .no-highlight { + display: block; + padding: 0.5em; + color: #333; +} + + +/* + * Additions specific to the search functionality provided by MkDocs + */ + +.search-results { + margin-top: 23px; +} + +.search-results article { + border-top: 1px solid #E1E4E5; + padding-top: 24px; +} + +.search-results article:first-child { + border-top: none; +} + +form .search-query { + width: 100%; + border-radius: 50px; + padding: 6px 12px; + border-color: #D1D4D5; +} + +/* + * Improve inline code blocks within admonitions. + * + * https://github.com/mkdocs/mkdocs/issues/656 + */ + .rst-content .admonition code { + color: #404040; + border: 1px solid #c7c9cb; + border: 1px solid rgba(0, 0, 0, 0.2); + background: #f8fbfd; + background: rgba(255, 255, 255, 0.7); +} + +/* + * Account for wide tables which go off the side. + * Override borders to avoid weirdness on narrow tables. + * + * https://github.com/mkdocs/mkdocs/issues/834 + * https://github.com/mkdocs/mkdocs/pull/1034 + */ +.rst-content .section .docutils { + width: 100%; + overflow: auto; + display: block; + border: none; +} + +td, th { + border: 1px solid #e1e4e5 !important; + border-collapse: collapse; +} + +/* + * Without the following amendments, the navigation in the theme will be + * slightly cut off. This is due to the fact that the .wy-nav-side has a + * padding-bottom of 2em, which must not necessarily align with the font-size of + * 90 % on the .rst-current-version container, combined with the padding of 12px + * above and below. These amendments fix this in two steps: First, make sure the + * .rst-current-version container has a fixed height of 40px, achieved using + * line-height, and then applying a padding-bottom of 40px to this container. In + * a second step, the items within that container are re-aligned using flexbox. + * + * https://github.com/mkdocs/mkdocs/issues/2012 + */ + .wy-nav-side { + padding-bottom: 40px; +} + +/* For section-index only */ +.wy-menu-vertical .current-section p { + background-color: #e3e3e3; + color: #404040; +} + +/* + * The second step of above amendment: Here we make sure the items are aligned + * correctly within the .rst-current-version container. Using flexbox, we + * achieve it in such a way that it will look like the following: + * + * [No repo_name] + * Next >> // On the first page + * << Previous Next >> // On all subsequent pages + * + * [With repo_name] + * Next >> // On the first page + * << Previous Next >> // On all subsequent pages + * + * https://github.com/mkdocs/mkdocs/issues/2012 + */ +.rst-versions .rst-current-version { + padding: 0 12px; + display: flex; + font-size: initial; + justify-content: space-between; + align-items: center; + line-height: 40px; +} + +/* + * Please note that this amendment also involves removing certain inline-styles + * from the file ./mkdocs/themes/readthedocs/versions.html. + * + * https://github.com/mkdocs/mkdocs/issues/2012 + */ +.rst-current-version span { + flex: 1; + text-align: center; +} diff --git a/eventloganalysis/index.html b/eventloganalysis/index.html new file mode 100644 index 000000000..c7e745212 --- /dev/null +++ b/eventloganalysis/index.html @@ -0,0 +1,309 @@ + + + + + + + + Analysis - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + + +
  • +
  • +
+
+
+
+
+ +

Analysis

+

The Event Logger outputs structured logs, as Json objects. These can be processed on any platform that can read Json files.

+

A sample log file is shown below.

+
[
+{"ticks":637799309335620180,"peer":"088edbc1-1d1f09b5","type":"Ubiq.Messaging.NetworkScene","event":"Awake","arg1":"DESKTOP-F1J0MRR","arg2":"System Product Name (ASUS)","arg3":"f73fe01b1e21031d49274a1491d1d6b5714c92e9"},
+{"ticks":637799309384207356,"peer":"088edbc1-1d1f09b5","type":"Ubiq.Voip.VoipPeerConnectionManager","objectid":"088edbc1-1d1f09b5","componentid":50,"event":"CreatePeerConnectionForPeer","arg1":"6c494697-2e79f5e3","arg2":"26a6ee77-3cec71fe"},
+{"ticks":637799309384277353,"peer":"088edbc1-1d1f09b5","type":"Ubiq.Voip.VoipPeerConnectionManager","objectid":"088edbc1-1d1f09b5","componentid":50,"event":"RequestPeerConnection","arg1":"6c494697-2e79f5e3","arg2":"26a6ee77-3cec71fe"},
+{"ticks":637799309087959820,"peer":"26a6ee77-3cec71fe","type":"Ubiq.Messaging.NetworkScene","event":"Awake","arg1":"Oculus Quest","arg2":"Oculus Quest","arg3":"b8db4746286db62ecad4c6fa13f17ab6"},
+{"ticks":637799309303272560,"peer":"26a6ee77-3cec71fe","type":"Ubiq.Voip.VoipPeerConnectionManager","objectid":"26a6ee77-3cec71fe","componentid":50,"event":"CreatePeerConnectionForRequest","arg1":"6c494697-2e79f5e3"}
+]
+
+

In this example, two peers - a desktop PC (Unity Editor) and an Oculus Quest - join a room. The NetworkScene and VoipPeerConnectionManager both log events.

+

Some Json members are defined by the Emitter type. For example, the ContextLogger writes the objectid of the context passed to it on creation. The arg members correspond to those passed to the Log() method. All entries include a timestamp and the Id of the Peer that generated the log. Timestamps are given in .Net Ticks.

+

Python

+

Python can be used to analys logs programmatically. The Jupyter notebook below shows how to import and process logs using Pandas, a powerful data analysis library for Python.

+

Image of Jupyter Notebook Code

+ +

Excel

+

Structured event logs are amenable to being viewed in a table. Microsoft Excel PowerQuery can import Json files and load events into Excel Worksheets.

+

To do this:

+
    +
  1. Open a new Workbook
  2. +
  3. From the Data tab, choose Get Data -> From File -> From Json
  4. +
  5. Open the log file, for example Application_log_2021-04-23-10-56-03_0.json
  6. +
  7. Select the List header and click Convert To Table. This will instruct Excel to treat each entry as a row.
  8. +
  9. Leave the Default Values in place and Click OK. The View will now appear as a Column.
  10. +
  11. Use the button in the top right to add the Expand Column step. This will split each record into a set of columns. Make sure to click Load More... if visible to ensure you get every possible field in the table.
  12. +
  13. Click OK
  14. +
  15. Click Close & Load to build your table.
  16. +
+

You can now order by Ticks, and filter columns such as Events.

+

Matlab

+

Like Python, Matlab can load Json using the jsondecode function.

+
% Read the text file and use jsondecode to produce a cell array of
+% structures.
+
+events = jsondecode(fileread("Debug_Log.json"));
+
+% The structures will have different fields, so we must use loops to filter
+% them before they can be combined into a single struct array or table.
+
+% Below, find all the events of type SpawnObject, and combine them into a
+% new array.
+
+spawn = [];
+
+for i = 1:numel(events)
+   % The curly braces access the contents of the cell i, which is the
+   % struct itself.
+   s = events{i};
+   if categorical(cellstr(s.event)) == categorical("SpawnObject")
+       spawn = [spawn; s];
+   end
+end
+
+% Convert the new array into a table
+T = struct2table(spawn);
+
+% Use the table to change the type of the sceneid column so we can easily
+% split the events by which peer they are from.
+T.peer = categorical(T.peer);
+
+% Filter the events to keep only those emitted by the Peer that initiated
+% the spawn
+T = T(T.arg3,:);
+
+% Plot the number of objects spawned over time, by each Peer
+figure;
+hold all;
+peers = unique(T.peer);
+for p = peers'
+   spawned = T(T.peer == p,:);
+   plot(spawned.ticks,1:size(spawned,1));
+end
+
+xlabel("Time (ticks)");
+ylabel("Number of Objects");
+legend(peers);
+
+

Matlab Plot

+ + +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/eventlogging/index.html b/eventlogging/index.html new file mode 100644 index 000000000..b77f08d24 --- /dev/null +++ b/eventlogging/index.html @@ -0,0 +1,357 @@ + + + + + + + + Introduction - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + + +
  • +
  • +
+
+
+
+
+ +

Introduction

+

Networked VR applications require different types of logging, such as:

+
    +
  1. Debug Logs
  2. +
  3. Experiment Logs
  4. +
  5. Network Traces
  6. +
+

+ +

    +
  1. Refers to logging expected and exceptional events that occur during a regular session. The purpose is post-hoc debugging of high-level application code.
  2. +
  3. Refers to logging application-specific data, such as measurements or questionnaire responses for an experiment.
  4. +
  5. Refers to captures of network traffic to investigate reproducible low-level netcode bugs.
  6. +
+

(1) & (2) are handled by Ubiq's Event Logging system. (3) has distinct performance implications, so is handled seperately.

+

Use Case

+

The Event Logging System is for collecting low or medium frequency events from multiple peers. The Event Logging system can log both Ubiq and third-party events, which can then be extracted and analysed.

+

Events are discrete, but otherwise have very few restrictions. It is up to the user to ensure that event logging in their application doesn't negatively affect performance.

+

Overview

+

Events are generated by LogEmitter instances placed throughout the application. Events generated by these components are passed to a local LogCollector instance. The LogCollector then writes them to disk (or database, or other endpoint), or forwards them to another LogCollector that will.

+

Data flow diagram of the Event Logging System

+

LogEmitter

+

LogEmitter instances are lightweight objects that the application uses to log events. +Calls to a LogEmitter are expected to be placed throughout the system persistently, rather than gated with pre-processor defines.

+

The most common types of event logger are the ContextEventLogger, which is designed to work with Components that have a NetworkContext, and the ExperimentEventLogger, designed for logging measurements in experiments.

+
public class VoipPeerConnectionManager : MonoBehaviour
+{
+    private ContextLogEmitter debug;
+
+    private void Start()
+    {
+        context = NetworkScene.Register(this);
+        debug = new ContextLogEmitter(context);
+    }
+
+    public void ProcessMessage(ReferenceCountedSceneGraphMessage message)
+    {
+        var msg = JsonUtility.FromJson<Message>(message.ToString());
+        switch (msg.type)
+        {
+            case "RequestPeerConnection":
+                debug.Log("CreatePeerConnectionForRequest", msg.objectid);
+                break;
+        }
+    }
+}
+
+

The snippet above demonstrates the creation and use of a ContextEventLogger. The VoipPeerConnectionManager declares the ContextLogEmitter debug and initialises it with a ContextEventLogger once a context has been created. The Log method can then be called to log the receipt of a specific message.

+

Events are recorded as Json objects, for example:

+
{
+"ticks":637794978937526996,
+"peer":"aa9a2c8c-0c6f11c7",
+"type":"Ubiq.Voip.VoipPeerConnectionManager",
+"objectid":"aa9a2c8c-0c6f11c7",
+"componentid":50,
+"event":"CreatePeerConnectionForRequest",
+"arg1":"9020d814-45c41c19"
+}
+
+

LogEmitter instances attach to a single LogCollector. The emitter constructors find the closest LogCollector automatically.

+

LogEmitter methods can be safely called from outside the Unity main thread. They should not be called from outside CLR threads however.

+

LogEmitter instances are designed to have zero overhead when logs are not actually written. The Log method has many overloads to avoid boxing, and serialisation only runs when logging is on. Logs are only written when there is a listening LogCollector in the scene. +If the LogCollector is disabled, non-existent, or the Collector's EventFilter ignores the emitter's event type, the emitter will not do anything.

+

It is encouraged to make as many LogEmitter instances as needed. Individual emitters are simple, with few options. Use multiple LogEmitter instances within a class to get fine-grained control over logging, for example different log levels.

+

Events and Filters

+

A LogEmitter can tag events with a flag. A number of flags are pre-defined in Ubiq.Logging.EventType enum. The underlying type is an sbyte, so additional values can be used in an unchecked context.

+
[Flags]
+enum EventType
+{
+    Application = 1,
+    Experiment = 2,
+    Debug = 4,
+    Info = 8
+}
+
+

A LogCollector can be set to ignore one or more of these types with its EventsFilter member. If a flag is set the event is logged. If it is unset the event is not logged. Filtering applies to local events. LogCollectors will forward and write all external events regardless of the filter. Filtered events are not cached, but lost permanently.

+

LogCollector writes events of different types to different streams/files. Prefixes are defined for the entries in Ubiq.Logging.EventType. Other tags will display as their numerical equivalent.

+

The LogCollector EventsFilter member and the Tag member of any LogEmitter can be changed at any time.

+

ComponentEventLogger and ContextEventLogger have their type set to Debug by default. Convenience classes are defined for ExperimentLogEmitter and InfoLogEmitter.

+

LogCollector

+

LogCollector instances receive events from local LogEmitter instances and remote LogCollector instances. They do one of three things with these events:

+
    +
  1. Cache them (Buffering Mode)
  2. +
  3. Write them to a local file (Writing Mode)
  4. +
  5. Forward them to another LogCollector (Forwarding Mode)
  6. +
+

The behaviour depends on the value of the destination member, which determines where the events should go.

+

LogCollector instances can be organised this way into complex tree arrangements.

+

Possible Tree Diagram of LogCollector Hierarchy

+

However, the expected use case is that Peers in a Group forward events directly to one Collector, that also writes them to disk.

+

Expected LogCollector Hierarchy

+

LogCollector writes events to files in the Persistent Data folder of whatever platform it is running on. It does this through a set of Ubiq.Logging.LogCollector.IOutputStream instances (one for each event type, created on demand).

+

The filenames include the event type and a timestamp.

+

Buffering

+

When a LogCollector is not Forwarding or Writing, it will cache or Buffer all events received (whether local or remote). If a LogCollector was previously Writing or Forwarding and stops, it will resume Buffering. When a LogCollector begins Writing or Forwarding, it will start with all the events in its buffer.

+

Events only leave the buffer when being sent to a safe destination (another LogCollector, or a file). In this way logging is lossless, regardless of the initialisation order and even when changing the Active LogCollector.

+

However, events that leave the buffer are gone permanently. If a LogCollector writes events, then another LogCollector becomes active at a later time, the second will only output events received after it became active. The file from the first LogCollector must be retrieved out-of-band to have the complete record of events.

+

This can be avoided by ensuring only one LogCollector becomes active during the lifetime of the Peer Group.

+

Encodings

+

Events are written as Json (Utf8 strings).

+

LogCollector attempts to make the output files Json compliant, by placing events in a top-level Json Array, with comma seperated entries. If the file is being read live however, or the process terminates unexpectedly, the file will not have the closing bracket ( ] ).

+

In this case the application reading the files should either add the bracket to the end of the stream or perform its own tokenisation.

+

Changing the LogCollector

+

Only one LogCollector in a Peer Group may be in Writing Mode at any time.

+

The Active Collector can be changed by calling StartCollection on the new Collector. This can be done at any time.

+

When the Active Collector changes, the Peers must agree on the new Collector. This is a Distributed Agreement problem. LogCollector solves this using the Global Snapshot method [1].

+

The new Collector broadcasts a message with a logical clock value and atomically updates its cut state. When Peers receive the message, they compare the clock against their own. If it is greater, they update their cut state, otherwise the message is ignored. +If a Collector is attempting to become Active, and receives a message with the same clock value as its own, a collision has occured. In this case, the Collector re-initialises its clock to a random value, and re-transmits its state. +If a message is received with a greater clock value, it updates its own State as if it were any other Peer.

+

This algorithm assumes that Peers are fully connected, and message passing is reliable, which is true in Ubiq. +The logical clock ensures that all Peers converge on the same State, regardless of the order the messages were recieved in. The Peers have converged when all messages have been delivered.

+

When the Active Collector leaves the Peer Group, the Clock is reset.

+

Limitations

+

As long as Peers remain connected, the system is lossless, even during convergence.

+

However, during convergence different events may flow to the old and new Active Collectors at the same time, resulting in events being spread between the sets of log files. This will occur until convergence. There is no upper bound on this time, and there is no mechanism to check if convergence has been achieved, so StartCollection should be called with care.

+

To surrender position as the Active Collector, a Collector may call StopCollection, however the time between this and the Peers converging to null is again indeterminate.

+

When the Active Collector changes, the delay between the previous Collector receiving the new state, and other Peers in the Group receiving the new state, may cause it to behave as a relay for a time.

+

Process Failure

+

If the Active Collector process fails, log events will be lost until the system converges on a new Collector or null.

+

If the Collector was part of a Room, the Rooms system can detect disconnection in some cases, and Collectors will update their cut state independently. The Collectors in this case will cache until a new Collector volunteers itself.

+

If a process that was not the Active Collector fails, then that process will simply not emit events. If that process was previously an Active Collector, it is still possible to lose events if the Collector was acting as a relay when the process failed.

+

For collecting data such as experimental logs, it is recommended to only ever have one collector on a process that can be monitored. For example, the logcollectorservice.

+

Verification

+

The LogCollector method GetBufferedEventCount returns the number of Events currently buffered. As LogCollector is multi-threaded, this count may change even while it is being returned. However, if it is known, for example, that an application will not generate any new Events of a particular type, it can be used to check whether all of those events have finished writing.

+

The LogCollector method Ping will ping the Active Collector.

+

All Log messages are delivered in order. Therefore these mechanisms can be used together to verify that all log messages for a particular application have been delivered successfully before it exits.

+

To do this, an application could:

+
    +
  1. Write the last Event, e.g. a Questionnaire result.
  2. +
  3. In the same thread, wait until the number of buffered events of that type is zero.
  4. +
  5. Ping the Active Collector
  6. +
+

When a response is receieved, the Event, and all preceeding it, will have been successfully delivered and the application can safely exit.

+

This protocol is implemented in the WaitForTransmitComplete method.

+

This does not protect against process failure of an intermediary LogCollector however, a condition which is irrecoverable.

+

Reliability

+

In order for WaitForTransmitComplete to confirm that an event has been successfully delivered, the integrity of the LogCollector processes must be fully visible. One way to achieve this is to ensure only one LogCollector is ever active, and that that LogCollector never forwards. In this case, if the Ping is received, then the logs must also be successfully delivered as they cannot have taken any other path to the collector.

+

LogCollector will keep track of this by default, and warn any caller of WaitForTransmitComplete if this is the case.

+

Analysis

+

A LogCollector outputs a stream of structured logs in compliant Json. These logs can be fed to a stack like the ELK, processed with third-party tools like Matlab or Excel, or processed programmatically on platforms such as Python.

+

See the Analysis section for examples of how to process the logs.

+

[1] Kshemkalyani, A. D., & Singhal, M. (2008). Distributed Computing: Principles, Algorithms and Practice. Cambridge University Press.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/eventloggingcollectorservice/index.html b/eventloggingcollectorservice/index.html new file mode 100644 index 000000000..9a4369eb5 --- /dev/null +++ b/eventloggingcollectorservice/index.html @@ -0,0 +1,236 @@ + + + + + + + + LogCollector Service - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + + +
  • +
  • +
+
+
+
+
+ +

Log Collector Service

+

The LogCollectorService is an example NodeJs application that joins a room and writes all the Experiment Log Events (0x2) in that Room to disk.

+

The sample is located in the Node/samples/logcollectorservice directory.

+

Use Case

+

The Log Collector Service can be used to automatically collect data from self-directed experiments.

+

To do this, an experimentor would create a build that automatically joined a pre-defined Room. Additionally, the Log Collector Service would be started on a server and configured to join the same Room.

+

As participants ran their builds and completed the experiment, they would all join the same room and forward their events to the LogCollector running in the Log Collector Service, which would write those events to disk on the server.

+

Questionnaire Scene

+

A good way to try the Log Collector Service is to use the Questionnaire Sample (Samples/Single/Questionnaire).

+

Add the Join Room Component to the NetworkScene, and set the Room GUID to the same one as in the logcollectorservice app.

+

Be sure to generate a new GUID to avoid collisions with others potentially trying the same demonstration.

+

Then, start and stop the Questionnaire scene, submitting the Questionnaire a few times each run.

+

In the logcollectorservice directory, a number of log files should be created, with the Ids that the Peer took on each time it started.

+

Configuration

+

The logcollectorservice application is configured by changing the source code of app.js. The two variables that are likely to change are the log event type, and the room GUID.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/eventloggingexperimentquestionnaire/index.html b/eventloggingexperimentquestionnaire/index.html new file mode 100644 index 000000000..daedf07e6 --- /dev/null +++ b/eventloggingexperimentquestionnaire/index.html @@ -0,0 +1,293 @@ + + + + + + + + Questionnaire - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + + +
  • +
  • +
+
+
+
+
+ +

Questionnaire

+

The Questionnaire Sample (Samples/Single/Questionnaire) shows how the Event Logging System may be used to collect questionnaire responses.

+

This scene contains a panel with an example Component, Questionnaire attached to it. The Component iterates over all Slider instances under its GameObject, and uses an ExperimentLogEmitter to write their values when the user clicks Done.

+
public class Questionnaire : MonoBehaviour
+{
+    LogEmitter results;
+
+    // Start is called before the first frame update
+    void Start()
+    {
+        results = new ExperimentLogEmitter(this);
+    }
+
+    public void Done()
+    {
+        foreach (var item in GetComponentsInChildren<Slider>())
+        {
+            results.Log("Answer", item.name, item.value);
+        }
+    }
+
+}
+
+

The Questionnaire Sample Scene in the Editor

+

The Questionnaire can be completed locally in Play Mode. Alternatively, the scene can be run remotely, and the experimentor in the Editor can join the same room as the remote copy. In either case, the experimentor in the Editor can click Start Collection on the NetworkScene > Log Manager > LogCollector to recieve the Questionnaire results.

+

The experimentor can click Start Collection before or after the questionnaire has been completed, and the participant can complete the Questionnaire before or after joining the room. In all cases the results will be receieved correctly.

+

Sample Output

+

Below is the resulting Experiment log file from an application built with the Questionnaire scene.

+
[
+{"ticks":637795003787005516,"peer":"f7d98080-7c7b05ca","event":"Answer","arg1":"Slider 1","arg2":0.707253},
+{"ticks":637795003787045512,"peer":"f7d98080-7c7b05ca","event":"Answer","arg1":"Slider 2","arg2":0.30657154},
+{"ticks":637795003787045512,"peer":"f7d98080-7c7b05ca","event":"Answer","arg1":"Slider 3","arg2":0.7034317}
+]
+
+

The Questionnaire was filled in on an Oculus Quest, after joining the same room as a user running the same scene in the Unity Editor. As soon as the Questionnaire was completed, the Unity Editor user could find the Experiment log by clicking the Open Folder button of the LogCollector Component in the Editor.

+

Log Collector Inspector at Runtime

+

Since no filters were set up on the LogManager, a Debug log for the session is also created in the same folder.

+
[
+{"ticks":637795043778071253,"peer":"cbc6f82b-24ec48b3","type":"Ubiq.Messaging.NetworkScene","event":"Awake","arg1":"DESKTOP-F1J0MRR","arg2":"System Product Name (ASUS)","arg3":"f73fe01b1e21031d49274a1491d1d6b5714c92e9"},
+{"ticks":637795044161926844,"peer":"cbc6f82b-24ec48b3","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":2,"arg2":"1e38967c-7a5701a3","arg3":true},
+{"ticks":637795044161966844,"peer":"cbc6f82b-24ec48b3","type":"Ubiq.Voip.VoipPeerConnectionManager","objectid":"cbc6f82b-24ec48b3","componentid":50,"event":"CreatePeerConnectionForPeer","arg1":"2a865340-80169ce2","arg2":"4641730f-148936d7"},
+{"ticks":637795044162026839,"peer":"cbc6f82b-24ec48b3","type":"Ubiq.Voip.VoipPeerConnectionManager","objectid":"cbc6f82b-24ec48b3","componentid":50,"event":"RequestPeerConnection","arg1":"2a865340-80169ce2","arg2":"4641730f-148936d7"},
+{"ticks":637795044162066856,"peer":"cbc6f82b-24ec48b3","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":2,"arg2":"effadbc0-a6beab2b","arg3":false},
+{"ticks":637795043937235620,"peer":"4641730f-148936d7","type":"Ubiq.Messaging.NetworkScene","event":"Awake","arg1":"Oculus Quest","arg2":"Oculus Quest","arg3":"b8db4746286db62ecad4c6fa13f17ab6"},
+{"ticks":637795044080181550,"peer":"4641730f-148936d7","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":2,"arg2":"effadbc0-a6beab2b","arg3":true},
+{"ticks":637795044152929360,"peer":"4641730f-148936d7","type":"Ubiq.Voip.VoipPeerConnectionManager","objectid":"4641730f-148936d7","componentid":50,"event":"CreatePeerConnectionForRequest","arg1":"2a865340-80169ce2"},
+{"ticks":637795044153061850,"peer":"4641730f-148936d7","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":2,"arg2":"1e38967c-7a5701a3","arg3":false}
+]
+
+

Graceful Exit

+

The Questionnaire Panel also has a Quit button. This button makes use of the LogCollector WaitForTransmitComplete method to quit the application, but only when the questionnaire results have been successfully delivered.

+
        public void Quit()
+        {
+            LogCollector.Find(this).WaitForTransmitComplete(results.EventType, ready =>
+            {
+                if(!ready)
+                {
+                    // Here it may be desirable to to save the logs another way
+                    Debug.LogWarning("ActiveCollector changed or went away: cannot confirm logs have been delivered!");
+                }
+#if UNITY_EDITOR
+                UnityEditor.EditorApplication.isPlaying = false;
+#else
+                Application.Quit();
+#endif
+            });
+        }
+
+

The callback will only be called once the Experiment logs have left the local LogCollector.

+

Enter Play Mode, click Done, then click Quit.

+

Since the LogCollector is buffering, the application won't exit because the Log Events are still in Memory. Click Start Collection and the application will immediately write the logs and exit.

+

Log Collector Showing Events in Buffer

+

Try as well again entering Play Mode, and clicking Done and Quit. This time however join a Room with a LogCollector on another Peer. As soon as that Peer's LogCollector is Started, the Questionnaire will quit.

+

If the LogCollector on the other Peer is already active (e.g. in the case of a running logcollectorservice), the application will quit almost as soon as it joins the Room.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/eventlogginggettingstarted/index.html b/eventlogginggettingstarted/index.html new file mode 100644 index 000000000..499962225 --- /dev/null +++ b/eventlogginggettingstarted/index.html @@ -0,0 +1,321 @@ + + + + + + + + Logging - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Logging

+

Ubiq has the ability to record, forward and store logs. Ubiq itself generates logs, and custom components can create them too.

+

For example, the logging system could be used to record the answers to a questionnaire, or the direction of a user's gaze, and forward them to an experimentor.

+

This guide shows how to set up and log some simple data in the Hello World scene.

+

Log Flow

+

Log events (such as answering a question) are generated by Log Emitters with a simple call, e.g. debug.Log("MyEvent"). These events are received by a Log Collector. LogCollector is a Component belonging to a NetworkScene. Depending on where logs are finally written, the local LogCollector may forward events to another LogCollector in the same room, or write them directly to a file.

+

Log Events Data Flow

+

There can be many Log Emitters in an application. There should be one LogCollector per Peer. Only one LogCollector should be writing at a time.

+

LogManager Attached to a NetworkScene Hierarchy

+

Creating a Questionnaire Button

+

Log events can come from any source. In this guide, they will be generated when a user presses a button.

+

Create a new Button in the scene. Below, a new GameObject was added to the Main Menu. Create a new script, ButtonLogger, and add it to the Button as well.

+

Adding a new Button over the Main Menu

+

The script for ButtonLogger is below.

+
using Ubiq.Logging;
+using UnityEngine;
+using UnityEngine.UI;
+
+public class ButtonLogger : MonoBehaviour
+{
+    ExperimentLogEmitter events;
+
+    // Start is called before the first frame update
+    void Start()
+    {
+        events = new ExperimentLogEmitter(this);
+        GetComponent<Button>().onClick.AddListener(OnButtonClicked);
+    }
+
+    void OnButtonClicked()
+    {
+        events.Log("Button Pressed");
+    }
+}
+
+

First, an ExperimentLogEmitter is declared. This is the object that will be used to emit log events. It is declared in the class but initialised in Start(). This is because it has to find the local Log Collector to communicate with, which can't be done until the scene initialisation begins.

+

Event Types

+

Events can be given a Type. The type hints at the meaning of the event. For example, Info events record how the application itself is working. Experiment events could record data for experiments. Debug events could record simple debugging information.

+

Any code can create any type of event. The type is used to filter events.

+

The Emitter created in the ButtonLogger script will generate Experiment events.

+

Log Collector

+

The default NetworkScene Prefab already contains a LogCollector, so there is no need to add this.

+

A callback is registered with the Button's OnClick event by the ButtonLogger script. When this is raised by the user clicking the button, a log event ("Button Pressed") is emitted.

+

Start the Scene and look at the Log Collector in the Inspector. As the Button is clicked the memory usage of the collector will increase, indicating that the Button is generating events.

+

Memory Usage of Log Manager increasing

+

Writing Logs

+

The events will remain in the LogCollector until they are requested.

+

Click Start Collection. The Entries count will increase, and opening the log folder will reveal an Experiment log, with a number of Button Pressed events.

+

Log Files Created in AppData

+
[{"ticks":637795015469208026,"peer":"f6aa7d01-24da1cf2","event":"Button Pressed"},
+{"ticks":637795015470638029,"peer":"f6aa7d01-24da1cf2","event":"Button Pressed"},
+{"ticks":637795015471998382,"peer":"f6aa7d01-24da1cf2","event":"Button Pressed"},
+{"ticks":637795015473438942,"peer":"f6aa7d01-24da1cf2","event":"Button Pressed"},
+{"ticks":637795015474853269,"peer":"f6aa7d01-24da1cf2","event":"Button Pressed"},
+{"ticks":637795015476313220,"peer":"f6aa7d01-24da1cf2","event":"Button Pressed"},
+{"ticks":637795015477743218,"peer":"f6aa7d01-24da1cf2","event":"Button Pressed"},
+{"ticks":637795015479173222,"peer":"f6aa7d01-24da1cf2","event":"Button Pressed"},
+{"ticks":637795015480802962,"peer":"f6aa7d01-24da1cf2","event":"Button Pressed"},
+{"ticks":637795015482232892,"peer":"f6aa7d01-24da1cf2","event":"Button Pressed"}
+
+

Logging Arguments

+

The LogEmitter::Log() method can take a number of arguments in addition to the event name.

+

Add a new member to the ButtonLogger, AnswerName, and pass it in as an argument.

+
    public string Answer;
+
+    void OnButtonClicked()
+    {
+        events.Log("Button Pressed", Answer);
+    }
+
+

The value of Answer can be set up in the inspector. Duplicate the Button and set two different values of Answer for each.

+

Creating Additional Buttons

+

Now, when looking at the log after pressing the buttons it will show the value of Answer as well.

+
[{"ticks":637795019171902297,"peer":"ba742247-1415eb07","event":"Button Pressed","arg1":"Yes"},
+{"ticks":637795019174622297,"peer":"ba742247-1415eb07","event":"Button Pressed","arg1":"Yes"},
+{"ticks":637795019180442303,"peer":"ba742247-1415eb07","event":"Button Pressed","arg1":"No"},
+{"ticks":637795019183882312,"peer":"ba742247-1415eb07","event":"Button Pressed","arg1":"No"},
+{"ticks":637795019201742296,"peer":"ba742247-1415eb07","event":"Button Pressed","arg1":"Yes"}
+
+

Practically any variable that can be turned into a string can be logged this way.

+

Collecting from a Distributed Experiment

+

So far the LogCollector has just collected from the local player.

+

Create a Build of the Hello World application and run it, then press the buttons a few times.

+

Note that so far, the application has not even joined a room. This is OK because the LogCollector will hold all logs until they are requested.

+

Next, press Play to load the Hello World Scene in the Editor.

+

Now have both Peers join the same room (new or old, in any order). When both have joined, the Avatars of the other Peer should be visible in each.

+

Editor and Stanadlone Build Peers in a Room

+

In the Editor, navigate to the LogCollector and click on Start Collection in the Inspector.

+

The Entries count will increase, and an Experiment Log file will appear in the default Logs Folder, containing any answers entered in both the Editor and Standalone Build.

+

Considerations

+

To find out more about the logging, see the Logging section in the Advanced topics.

+

Log events can be generated from user actions, but also other external events, or at a regular frequency (e.g. to log the Transform of dynamic objects)

+

You can change the active LogCollector at runtime losslessly, so long as no Peers fail or unexpectedly disconnect.

+

Collection can also be started programmatically, in addition to clicking Start Collection. This allows experiment code to start collection other ways, including in Standalone builds.

+

See the Samples/Single/Questionnaire sample for a complete Questionnaire implementation.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/eventloggingunittests/index.html b/eventloggingunittests/index.html new file mode 100644 index 000000000..124927689 --- /dev/null +++ b/eventloggingunittests/index.html @@ -0,0 +1,225 @@ + + + + + + + + Unit Tests - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + + +
  • +
  • +
+
+
+
+
+ +

Event Logging Unit Tests

+

The LoggingDiagnostics scene (Samples/Single/Logging) performs stress testing of the Logging system.

+

This scene contains a Component, LoggingDiagnostics, and corresponding UI in the scene to control it outside the Editor.

+

The Component creates a number of LogEmitters that generate arbitrary, deterministic events. Additionally, the Component will instruct the Peer's LogCollector to become the primary Collector at random.

+

When multiple instances of this Peer are running, each will create and forward events to eachother as the primary mode moves between them.

+

As events are deterministic, the log files can be checked once all Peers have shut down to verify that all events from all peers were logged correctly, despite changing the primary Collector and starting at different times.

+

The function to check the logs is also in the LoggingDiagnostics Component and is invoked in the Inspector in the Editor.

+

This code assumes that all test Peers were running on the same machine, as it will check all files in the systems persistent data path.

+

If some Peers were running elsewhere (e.g. on an Oculus Quest), those log files should be copied to the persistent data path first so their events will be included.

+

The user should quit the peers using the Finish & Exit UI button in the scene. Pressing this button will shut down all Peers in the Peer Group running the LoggingDiagnostics scene. This mechanism must be used as it delays the shutdown for a few seconds after the last log messages have been transmitted, to ensure enough time for the data to be logged and preventing false negatives when checking the data for integrity.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/eventlogserialisation/index.html b/eventlogserialisation/index.html new file mode 100644 index 000000000..9c0ea6522 --- /dev/null +++ b/eventlogserialisation/index.html @@ -0,0 +1,263 @@ + + + + + + + + Serialisation and Memory - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + + +
  • +
  • +
+
+
+
+
+ +

Serialiser

+

The logging functionality uses a custom Json serialiser that facilitates building Json objects across multiple function calls.

+

This is based on neuecc's Utf8Json, but with modifications to track memory usage and remove code generation requirements.

+

The Utf8Json serialiser is in the Ubiq.Logging.Utf8Json namspace. It is not recommended to use the serialiser for purposes other than logging; import an unmodified version of the library separately instead.

+

Formatters

+

Libraries such as Utf8Json typically have methods that serialise and deseralise specific types by sequentially reading and writing tokens to and from streams. (In this case, the tokes are read and written using the JsonReader and JsonWriter structures.)

+

Utf8Json finds the appropriate method to use using FormatterResovler classes. These classes return a cached Formatter<T> class, which is an object with two methods to read and write objects of type T as Json.

+

The included version of Utf8Json includes formatters for a number of known types, including all the basic primitives, and enums. Enums are serialised as names.

+

Code Generation

+

To serialise types that do not have an explicit formatter defined, libraries such as Utf8Json usually build serialisation methods at runtime using code generation. This is not supported on platforms that use IL2CPP however.

+

To avoid code generation, unknown types are serialised by the Unity JsonUtility and embedded as objects.

+

Resolvers and Formatters

+

When a type is serialised, Utf8Json will use the DefaultResolver to find a formatter. The DefaultResolver is defined in the JsonSerializer class as a static member and returns a StandardResolver, a type of composite resolver. This resolver will search each resolver registered to it in turn, and return the first Formatter that matches the type. The StandardResolver includes formatters for the built-in types, and the dynamic formatter fallback.

+

Caching

+

Utf8Json makes common use of the following design pattern.

+
public IJsonFormatter<T> GetFormatter<T>()
+{
+    return FormatterCache<T>.formatter;
+}
+
+static class FormatterCache<T>
+{
+    public static readonly IJsonFormatter<T> formatter;
+
+    static FormatterCache()
+    {
+        formatter = (IJsonFormatter<T>)BuiltinResolverGetFormatterHelper.GetFormatter(typeof(T));
+    }
+}
+
+

This snippet leverages the behaviour of generics in C# to replace formatter references in code, without using code generation. +In C#, when a generic type is first constructed, the runtime will produce the concrete type and substitute it in the appropriate locations in the MSIL. +The static constructor is called before the formatter is referenced for the first time.

+

That is, the generic FormatterCache type is replaced in the MSIL and the formatter member it returns is resolved on demand (when the FormatterCache<T> is first constructed).

+

Memory Management

+

The Utf8Json namespace manages its own global memory pools to minimise GC allocations. It does not track memory usage directly however.

+

Instead, LogCollector instances track how many bytes of pooled memory they have in their queues at any time, and use this to control whether new events are buffered or dropped.

+

Memory is rented from the pool on demand by JsonWriter objects created by LogEmitter instances. Outstanding memory is returned to the pool when a JsonWriter is disposed. JsonWriters are disposed by the LogCollector they are fed to, either after being copied for transmission or discarded when the buffer reaches capacity. LogEmitter instances only create JsonWriters if a LogCollector has been registered to recieve (and dispose of) the completed object.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/files/Debug_Log.ipynb b/files/Debug_Log.ipynb new file mode 100644 index 000000000..314ecaa2b --- /dev/null +++ b/files/Debug_Log.ipynb @@ -0,0 +1,658 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "15d5dc47", + "metadata": {}, + "source": [ + "Print the version to check that Pandas is available.\n", + "If the module is not found, install pandas with `pip install pandas`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "18226a2f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.4.0\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "print(pd.__version__)" + ] + }, + { + "cell_type": "markdown", + "id": "d014d5f3", + "metadata": {}, + "source": [ + "Import the Json file with read_json. This will make a Pandas [*DataFrame*](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html) (a table)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "1dd112b3", + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.read_json(\"Debug_Log.json\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "5be4622f", + "metadata": {}, + "source": [ + "Print the first five rows to see what the table looks like." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "5c70ce3e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
tickspeertypeeventarg1arg2arg3objectidcomponentid
06377993137414760332990c448-6701d991Ubiq.Messaging.NetworkSceneAwakeDESKTOP-F1J0MRRSystem Product Name (ASUS)f73fe01b1e21031d49274a1491d1d6b5714c92e9NaNNaN
16377993138909156972990c448-6701d991Ubiq.Voip.VoipPeerConnectionManagerCreatePeerConnectionForPeer0b6034cb-5c98087221119b9e-9028aafaNaN2990c448-6701d99150.0
26377993138909757132990c448-6701d991Ubiq.Voip.VoipPeerConnectionManagerRequestPeerConnection0b6034cb-5c98087221119b9e-9028aafaNaN2990c448-6701d99150.0
36377993138910157012990c448-6701d991Ubiq.Samples.NetworkSpawnerSpawnObject2b0edec0e-fcf7792aTrue7725a971-a369264349018.0
46377993138910556952990c448-6701d991Ubiq.Samples.NetworkSpawnerSpawnObject243b53edd-7c900c8fFalse7725a971-a369264349018.0
\n", + "
" + ], + "text/plain": [ + " ticks peer type \\\n", + "0 637799313741476033 2990c448-6701d991 Ubiq.Messaging.NetworkScene \n", + "1 637799313890915697 2990c448-6701d991 Ubiq.Voip.VoipPeerConnectionManager \n", + "2 637799313890975713 2990c448-6701d991 Ubiq.Voip.VoipPeerConnectionManager \n", + "3 637799313891015701 2990c448-6701d991 Ubiq.Samples.NetworkSpawner \n", + "4 637799313891055695 2990c448-6701d991 Ubiq.Samples.NetworkSpawner \n", + "\n", + " event arg1 arg2 \\\n", + "0 Awake DESKTOP-F1J0MRR System Product Name (ASUS) \n", + "1 CreatePeerConnectionForPeer 0b6034cb-5c980872 21119b9e-9028aafa \n", + "2 RequestPeerConnection 0b6034cb-5c980872 21119b9e-9028aafa \n", + "3 SpawnObject 2 b0edec0e-fcf7792a \n", + "4 SpawnObject 2 43b53edd-7c900c8f \n", + "\n", + " arg3 objectid componentid \n", + "0 f73fe01b1e21031d49274a1491d1d6b5714c92e9 NaN NaN \n", + "1 NaN 2990c448-6701d991 50.0 \n", + "2 NaN 2990c448-6701d991 50.0 \n", + "3 True 7725a971-a3692643 49018.0 \n", + "4 False 7725a971-a3692643 49018.0 " + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.head(5)" + ] + }, + { + "cell_type": "markdown", + "id": "4bc3369d", + "metadata": {}, + "source": [ + "We can use Pandas to filter and process the structured logs. Use Unique to find all the event types seen during the session." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "2b55c17c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array(['Ubiq.Messaging.NetworkScene',\n", + " 'Ubiq.Voip.VoipPeerConnectionManager',\n", + " 'Ubiq.Samples.NetworkSpawner'], dtype=object)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df.type.unique()" + ] + }, + { + "cell_type": "markdown", + "id": "c03241ae", + "metadata": {}, + "source": [ + "Pandas can perform vector comparisons, and filter DataFrames by row indices. Select all the SpawnObject events." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "05170ba7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
tickspeertypeeventarg1arg2arg3objectidcomponentid
36377993138910157012990c448-6701d991Ubiq.Samples.NetworkSpawnerSpawnObject2b0edec0e-fcf7792aTrue7725a971-a369264349018.0
46377993138910556952990c448-6701d991Ubiq.Samples.NetworkSpawnerSpawnObject243b53edd-7c900c8fFalse7725a971-a369264349018.0
56377993139354757362990c448-6701d991Ubiq.Samples.NetworkSpawnerSpawnObject1bf000355-ece0ea90False7725a971-a369264349018.0
66377993139517757222990c448-6701d991Ubiq.Samples.NetworkSpawnerSpawnObject1ba22e255-c3d84eb4False7725a971-a369264349018.0
76377993139673257092990c448-6701d991Ubiq.Samples.NetworkSpawnerSpawnObject11ad3fc82-d41c3b8fFalse7725a971-a369264349018.0
\n", + "
" + ], + "text/plain": [ + " ticks peer type \\\n", + "3 637799313891015701 2990c448-6701d991 Ubiq.Samples.NetworkSpawner \n", + "4 637799313891055695 2990c448-6701d991 Ubiq.Samples.NetworkSpawner \n", + "5 637799313935475736 2990c448-6701d991 Ubiq.Samples.NetworkSpawner \n", + "6 637799313951775722 2990c448-6701d991 Ubiq.Samples.NetworkSpawner \n", + "7 637799313967325709 2990c448-6701d991 Ubiq.Samples.NetworkSpawner \n", + "\n", + " event arg1 arg2 arg3 objectid componentid \n", + "3 SpawnObject 2 b0edec0e-fcf7792a True 7725a971-a3692643 49018.0 \n", + "4 SpawnObject 2 43b53edd-7c900c8f False 7725a971-a3692643 49018.0 \n", + "5 SpawnObject 1 bf000355-ece0ea90 False 7725a971-a3692643 49018.0 \n", + "6 SpawnObject 1 ba22e255-c3d84eb4 False 7725a971-a3692643 49018.0 \n", + "7 SpawnObject 1 1ad3fc82-d41c3b8f False 7725a971-a3692643 49018.0 " + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df[df.event == \"SpawnObject\"].head(5)" + ] + }, + { + "cell_type": "markdown", + "id": "059a464c", + "metadata": {}, + "source": [ + "The Shape member shows the size of the result" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "1dfceb39", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(22, 9)" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df[df.event == \"SpawnObject\"].shape" + ] + }, + { + "cell_type": "markdown", + "id": "9b48fbdc", + "metadata": {}, + "source": [ + "The Pandas [*merge*](https://pandas.pydata.org/pandas-docs/stable/user_guide/merging.html) method is used to perform inner and outer joins to relate different log events. We merge the `Awake` events with the local `SpawnObject` events based on the `NetworkScene` Ids (`peer`)." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "eef34af9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
ticks_xpeertype_xevent_xarg1_xarg2_xarg3_xobjectid_xcomponentid_xticks_ytype_yevent_yarg1_yarg2_yarg3_yobjectid_ycomponentid_y
06377993138910157012990c448-6701d991Ubiq.Samples.NetworkSpawnerSpawnObject2b0edec0e-fcf7792aTrue7725a971-a369264349018.0637799313741476033Ubiq.Messaging.NetworkSceneAwakeDESKTOP-F1J0MRRSystem Product Name (ASUS)f73fe01b1e21031d49274a1491d1d6b5714c92e9NaNNaN
16377993138910556952990c448-6701d991Ubiq.Samples.NetworkSpawnerSpawnObject243b53edd-7c900c8fFalse7725a971-a369264349018.0637799313741476033Ubiq.Messaging.NetworkSceneAwakeDESKTOP-F1J0MRRSystem Product Name (ASUS)f73fe01b1e21031d49274a1491d1d6b5714c92e9NaNNaN
26377993139354757362990c448-6701d991Ubiq.Samples.NetworkSpawnerSpawnObject1bf000355-ece0ea90False7725a971-a369264349018.0637799313741476033Ubiq.Messaging.NetworkSceneAwakeDESKTOP-F1J0MRRSystem Product Name (ASUS)f73fe01b1e21031d49274a1491d1d6b5714c92e9NaNNaN
36377993139517757222990c448-6701d991Ubiq.Samples.NetworkSpawnerSpawnObject1ba22e255-c3d84eb4False7725a971-a369264349018.0637799313741476033Ubiq.Messaging.NetworkSceneAwakeDESKTOP-F1J0MRRSystem Product Name (ASUS)f73fe01b1e21031d49274a1491d1d6b5714c92e9NaNNaN
46377993139673257092990c448-6701d991Ubiq.Samples.NetworkSpawnerSpawnObject11ad3fc82-d41c3b8fFalse7725a971-a369264349018.0637799313741476033Ubiq.Messaging.NetworkSceneAwakeDESKTOP-F1J0MRRSystem Product Name (ASUS)f73fe01b1e21031d49274a1491d1d6b5714c92e9NaNNaN
\n", + "
" + ], + "text/plain": [ + " ticks_x peer type_x \\\n", + "0 637799313891015701 2990c448-6701d991 Ubiq.Samples.NetworkSpawner \n", + "1 637799313891055695 2990c448-6701d991 Ubiq.Samples.NetworkSpawner \n", + "2 637799313935475736 2990c448-6701d991 Ubiq.Samples.NetworkSpawner \n", + "3 637799313951775722 2990c448-6701d991 Ubiq.Samples.NetworkSpawner \n", + "4 637799313967325709 2990c448-6701d991 Ubiq.Samples.NetworkSpawner \n", + "\n", + " event_x arg1_x arg2_x arg3_x objectid_x \\\n", + "0 SpawnObject 2 b0edec0e-fcf7792a True 7725a971-a3692643 \n", + "1 SpawnObject 2 43b53edd-7c900c8f False 7725a971-a3692643 \n", + "2 SpawnObject 1 bf000355-ece0ea90 False 7725a971-a3692643 \n", + "3 SpawnObject 1 ba22e255-c3d84eb4 False 7725a971-a3692643 \n", + "4 SpawnObject 1 1ad3fc82-d41c3b8f False 7725a971-a3692643 \n", + "\n", + " componentid_x ticks_y type_y event_y \\\n", + "0 49018.0 637799313741476033 Ubiq.Messaging.NetworkScene Awake \n", + "1 49018.0 637799313741476033 Ubiq.Messaging.NetworkScene Awake \n", + "2 49018.0 637799313741476033 Ubiq.Messaging.NetworkScene Awake \n", + "3 49018.0 637799313741476033 Ubiq.Messaging.NetworkScene Awake \n", + "4 49018.0 637799313741476033 Ubiq.Messaging.NetworkScene Awake \n", + "\n", + " arg1_y arg2_y \\\n", + "0 DESKTOP-F1J0MRR System Product Name (ASUS) \n", + "1 DESKTOP-F1J0MRR System Product Name (ASUS) \n", + "2 DESKTOP-F1J0MRR System Product Name (ASUS) \n", + "3 DESKTOP-F1J0MRR System Product Name (ASUS) \n", + "4 DESKTOP-F1J0MRR System Product Name (ASUS) \n", + "\n", + " arg3_y objectid_y componentid_y \n", + "0 f73fe01b1e21031d49274a1491d1d6b5714c92e9 NaN NaN \n", + "1 f73fe01b1e21031d49274a1491d1d6b5714c92e9 NaN NaN \n", + "2 f73fe01b1e21031d49274a1491d1d6b5714c92e9 NaN NaN \n", + "3 f73fe01b1e21031d49274a1491d1d6b5714c92e9 NaN NaN \n", + "4 f73fe01b1e21031d49274a1491d1d6b5714c92e9 NaN NaN " + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "spawn = df[df.event == \"SpawnObject\"]\n", + "awake = df[df.event == \"Awake\"]\n", + "f = pd.merge(spawn,awake,how=\"left\",left_on=\"peer\",right_on=\"peer\")\n", + "f.head(5)" + ] + }, + { + "cell_type": "markdown", + "id": "e6da9562", + "metadata": {}, + "source": [ + "We can perform arithmetic operations too. We use [string](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.Series.str.contains.html) operations, boolean arrays and [size](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.size.html) to find the number of distinct objects spawned by the Oculus Quest." + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "712540f1", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "5" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "is_quest = f.arg2_y.astype(str).str.contains(\"Quest\")\n", + "is_owner = f.arg3_x.astype(bool)\n", + "spawned_ids = f[is_quest & is_owner].arg2_x\n", + "spawned_ids.unique().size\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "98f8ffe4", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.4" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/files/Debug_Log.json b/files/Debug_Log.json new file mode 100644 index 000000000..cac5d85ed --- /dev/null +++ b/files/Debug_Log.json @@ -0,0 +1,27 @@ +[{"ticks":637799313741476033,"peer":"2990c448-6701d991","type":"Ubiq.Messaging.NetworkScene","event":"Awake","arg1":"DESKTOP-F1J0MRR","arg2":"System Product Name (ASUS)","arg3":"f73fe01b1e21031d49274a1491d1d6b5714c92e9"}, +{"ticks":637799313890915697,"peer":"2990c448-6701d991","type":"Ubiq.Voip.VoipPeerConnectionManager","objectid":"2990c448-6701d991","componentid":50,"event":"CreatePeerConnectionForPeer","arg1":"0b6034cb-5c980872","arg2":"21119b9e-9028aafa"}, +{"ticks":637799313890975713,"peer":"2990c448-6701d991","type":"Ubiq.Voip.VoipPeerConnectionManager","objectid":"2990c448-6701d991","componentid":50,"event":"RequestPeerConnection","arg1":"0b6034cb-5c980872","arg2":"21119b9e-9028aafa"}, +{"ticks":637799313891015701,"peer":"2990c448-6701d991","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":2,"arg2":"b0edec0e-fcf7792a","arg3":true}, +{"ticks":637799313891055695,"peer":"2990c448-6701d991","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":2,"arg2":"43b53edd-7c900c8f","arg3":false}, +{"ticks":637799313935475736,"peer":"2990c448-6701d991","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"bf000355-ece0ea90","arg3":false}, +{"ticks":637799313951775722,"peer":"2990c448-6701d991","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"ba22e255-c3d84eb4","arg3":false}, +{"ticks":637799313967325709,"peer":"2990c448-6701d991","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"1ad3fc82-d41c3b8f","arg3":false}, +{"ticks":637799314054015708,"peer":"2990c448-6701d991","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"012c25dc-08adb800","arg3":true}, +{"ticks":637799314081945711,"peer":"2990c448-6701d991","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"ce0b0011-3e59c779","arg3":true}, +{"ticks":637799314109495700,"peer":"2990c448-6701d991","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"50f7f74a-1026426c","arg3":false}, +{"ticks":637799314138725704,"peer":"2990c448-6701d991","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"5aa3c779-7c61e647","arg3":true}, +{"ticks":637799314157455705,"peer":"2990c448-6701d991","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"b9f9bb62-b19ae7b4","arg3":true}, +{"ticks":637799314177455768,"peer":"2990c448-6701d991","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"5ed09018-8b3ade5e","arg3":true}, +{"ticks":637799313643922770,"peer":"21119b9e-9028aafa","type":"Ubiq.Messaging.NetworkScene","event":"Awake","arg1":"Oculus Quest","arg2":"Oculus Quest","arg3":"b8db4746286db62ecad4c6fa13f17ab6"}, +{"ticks":637799313688217950,"peer":"21119b9e-9028aafa","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":2,"arg2":"43b53edd-7c900c8f","arg3":true}, +{"ticks":637799313809808810,"peer":"21119b9e-9028aafa","type":"Ubiq.Voip.VoipPeerConnectionManager","objectid":"21119b9e-9028aafa","componentid":50,"event":"CreatePeerConnectionForRequest","arg1":"0b6034cb-5c980872"}, +{"ticks":637799313810094950,"peer":"21119b9e-9028aafa","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":2,"arg2":"b0edec0e-fcf7792a","arg3":false}, +{"ticks":637799313853809540,"peer":"21119b9e-9028aafa","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"bf000355-ece0ea90","arg3":true}, +{"ticks":637799313870139360,"peer":"21119b9e-9028aafa","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"ba22e255-c3d84eb4","arg3":true}, +{"ticks":637799313885696490,"peer":"21119b9e-9028aafa","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"1ad3fc82-d41c3b8f","arg3":true}, +{"ticks":637799313972879660,"peer":"21119b9e-9028aafa","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"012c25dc-08adb800","arg3":false}, +{"ticks":637799314000867940,"peer":"21119b9e-9028aafa","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"ce0b0011-3e59c779","arg3":false}, +{"ticks":637799314027862800,"peer":"21119b9e-9028aafa","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"50f7f74a-1026426c","arg3":true}, +{"ticks":637799314057681120,"peer":"21119b9e-9028aafa","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"5aa3c779-7c61e647","arg3":false}, +{"ticks":637799314076340990,"peer":"21119b9e-9028aafa","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"b9f9bb62-b19ae7b4","arg3":false}, +{"ticks":637799314096397480,"peer":"21119b9e-9028aafa","type":"Ubiq.Samples.NetworkSpawner","objectid":"7725a971-a3692643","componentid":49018,"event":"SpawnObject","arg1":1,"arg2":"5ed09018-8b3ade5e","arg3":false}] \ No newline at end of file diff --git a/files/Debug_Log.m b/files/Debug_Log.m new file mode 100644 index 000000000..a4a9a3747 --- /dev/null +++ b/files/Debug_Log.m @@ -0,0 +1,45 @@ +% Read the text file and use jsondecode to produce a cell array of +% structures. + +events = jsondecode(fileread("Debug_Log.json")); + +% The structures will have different fields, so we must use loops to filter +% them before they can be combined into a single struct array or table. + +% Below, find all the events of type SpawnObject, and combine them into a +% new array. + +spawn = []; + +for i = 1:numel(events) + % The curly braces access the contents of the cell i, which is the + % struct itself. + s = events{i}; + if categorical(cellstr(s.event)) == categorical("SpawnObject") + spawn = [spawn; s]; + end +end + +% Convert the new array into a table +T = struct2table(spawn); + +% Use the table to change the type of the sceneid column so we can easily +% split the events by which peer they are from. +T.peer = categorical(T.peer); + +% Filter the events to keep only those emitted by the Peer that initiated +% the spawn +T = T(T.arg3,:); + +% Plot the number of objects spawned over time, by each Peer +figure; +hold all; +peers = unique(T.peer); +for p = peers' + spawned = T(T.peer == p,:); + plot(spawned.ticks,1:size(spawned,1)); +end + +xlabel("Time (ticks)"); +ylabel("Number of Objects"); +legend(peers); \ No newline at end of file diff --git a/generatinguniqueids/index.html b/generatinguniqueids/index.html new file mode 100644 index 000000000..5e026a0c5 --- /dev/null +++ b/generatinguniqueids/index.html @@ -0,0 +1,274 @@ + + + + + + + + Generating Unique Ids - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + + +
  • +
  • +
+
+
+
+
+ +

Unique Ids

+

One of the most important considerations when generating Ids is the probablity of collisions.

+

Collision Probability

+

Each client must be able to generate new Ids independently, to avoid dependencies on a server in peer-to-peer situations, and to avoid introducing deadlocks with the Unity programming model (such as needing a server to respond before all Start() calls can finish).

+

The probability of collisions between two independently well-generated identifiers is the Birthday Problem. The probability of a collision is given by,

+

+

On regular desktops, it is not possible to compute this for the typical numbers involved due to floating point quantisation. However there are approximations, such as the following which will work in Matlab,

+
    +
  • p = 1 - exp(-(n^2)/(2*(2^b)))
  • +
+

As the figure below shows, the probability of collisions depends on the number of objects in the scenario n, as well as the identifier length b (in bits). In Ubiq, the "scenario" is typically a room.

+

There are no rules defining acceptable chances of collisions. For example, OAuth 2 specifies a probability of less than 10e-160, which is not achievable even by 128-bit UUIDs.

+

In the case of a typical Ubiq room, which due to voice chat limitations can support approximately 25 users, a 32 bit value would probably be adequate. The concept of a Room in Ubiq can change depending on how it is used however, so Ubiq tries to avoid making assumptions about the potential collision space. If rooms were considered to be floating regions, for example, then the number of potential users could be much higher.

+

It is challenging to grasp levels of chance at these scales. The table below by Jeff Preshing presents collision probabilities more intuitively. (The numer of hash values below is the number of Objects between all users that could potentially connect in a Ubiq scenario, be that a room, set of rooms, a server, shard etc.)

+

A table describing small chances in human terms, e.g. 1 in 100 million are the odds of dying by shark attack.

+

Generating Random Identifiers

+

The collision probabilities above are only correct if identifiers are well-generated.

+

RFC 4122 descibes algorithms to generate 128-bit UUIDs. UUIDs are standardised identifiers on many network services. They are very popular as they can be independently generated quickly, while guaranteeing uniqueness and being amenable to storing, sorting and hashing. UUIDs can guarantee uniqueness beyond the probabilities discussed above because some versions include the use of MAC addresses, which are centrally registered (by the network card manufacturer) and which act as a "pre-shared secret".

+

RFC 4122 also presents an algorithm for version 4 which is based purely on random numbers. RFC 1750 discusses random numbers in more detail. Acquiring truly random numbers for identifier generation is challenging, as common computer systems rarely have APIs to access sources of truly random numbers. Most systems use deterministic generators of some kind, initialised with a seed. If there are dependencies between different sections of random number sequences, or covariances in the seed between peers, this will change the probability of collisions.

+

To make a good identifier, best practice is to use as many data sources as possible, with a strong mixing function. A strong mixer is one where each output bit is a complex, and different, function of all the input bits. The example given in RFC 4122 is to gather data sources into a buffer and use a message digest function such as SHA-1.

+

Identifiers and Bandwidth

+

A motivation for minimising id size is to reduce overhead. In a packet sending a Vector3 a 16 byte (a 128-bit UUID) overhead for the Id would have an overhead of 133% without even the Component Id. Though, on modern networks even a large relative overheads may not be problems so long as the total packet size is low. IPV6, for example, has a 40 byte header alone.

+

That is, reducing bandwidth by reducing identifier size is unlikely to be an optimal methodology. It should be expected that Ubiq header sizes increase over time, rather than decrease. Since Ubiq is based on a set of point-to-point connections, future works could consider techniques such as asymmetrical per-connection aliases to optimise bandwidth.

+

Object Ids in Ubiq

+

Size

+

Currently, Object Ids are represented by 64 bit numbers. This is because the collision probability for this length is very low for scenarios anticipated by Ubiq in the near future, but it is still easy to handle; 32 bit is too small, while 128 bit would require explicit serialisation/deserialisation, hashing and equality comparisons. 64 bit numbers can be stored in a long and nowadays even mobile platforms use 64 bit processors.

+

Unique Ids in Unity

+

Unity provides a number of data sources for hashing inside the Application and SystemInfo classes, however these are not accessible in class constructors. Therefore the .NET System.Environment, System.DateTime and System.Random classes are used.

+

References

+
    +
  • +

    Jesus, P., Baquero, C., & Almeida, P. (2006). ID Generation in Mobile Environments. 1–4. http://hdl.handle.net/1822/36065

    +
  • +
  • +

    https://neilmadden.blog/2018/08/30/moving-away-from-uuids/

    +
  • +
  • +

    https://preshing.com/20110504/hash-collision-probabilities/

    +
  • +
  • +

    https://tools.ietf.org/html/rfc4122

    +
  • +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/gettingubiq/index.html b/gettingubiq/index.html new file mode 100644 index 000000000..7e90a0771 --- /dev/null +++ b/gettingubiq/index.html @@ -0,0 +1,216 @@ + + + + + + + + Getting Ubiq - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Getting Ubiq

+

Ubiq can be downloaded from the UCL VR Github: https://github.com/UCL-VR/ubiq

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/html/b27a1510-38ec-4821-92b1-478dcbcf1ab1.html b/html/b27a1510-38ec-4821-92b1-478dcbcf1ab1.html new file mode 100644 index 000000000..844849e4c --- /dev/null +++ b/html/b27a1510-38ec-4821-92b1-478dcbcf1ab1.html @@ -0,0 +1,15322 @@ + + + + + +Debug_Log + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images/039f4355-b486-4e4a-a280-3b7646f25edf.svg b/images/039f4355-b486-4e4a-a280-3b7646f25edf.svg new file mode 100644 index 000000000..4f93019d6 --- /dev/null +++ b/images/039f4355-b486-4e4a-a280-3b7646f25edf.svg @@ -0,0 +1,416 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + Bot Peer + + + + Bots Controller + + + + Bots Manager + + + + Bot Peer + + + + Bot Peer + + + + + + + + + Control Room + Bots Room + Same Scene Graph + + + + Controlled overthe network + User Actions + + + diff --git a/images/099d09fc-c7ed-487e-895e-4c86033c964f.png b/images/099d09fc-c7ed-487e-895e-4c86033c964f.png new file mode 100644 index 000000000..667d7dc75 Binary files /dev/null and b/images/099d09fc-c7ed-487e-895e-4c86033c964f.png differ diff --git a/images/0f1c2fc3-2615-4f15-93c4-7533af6697eb.png b/images/0f1c2fc3-2615-4f15-93c4-7533af6697eb.png new file mode 100644 index 000000000..346802b4d Binary files /dev/null and b/images/0f1c2fc3-2615-4f15-93c4-7533af6697eb.png differ diff --git a/images/14008103-11ff-4307-a0a7-7b91ffaee82e.svg b/images/14008103-11ff-4307-a0a7-7b91ffaee82e.svg new file mode 100644 index 000000000..f2cd67d21 --- /dev/null +++ b/images/14008103-11ff-4307-a0a7-7b91ffaee82e.svg @@ -0,0 +1,777 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + Bot Peer + + + + Bots Controller + + + + Bots Manager + + + + Bot Peer + + + + Bot Peer + + + + + + + + + Control Room + Bots Room + Process #1 + + + + User Actions + + + Bot Peer + + + + Bots Manager + + + + Bot Peer + + + + Bot Peer + + + + + + + Bots Manager + + + + Bot Peer + + + + Bot Peer + + + + + + + Process #2 + + Process #3 + + + diff --git a/images/16de1600-91ac-4377-9aae-c3b66b300889.svg b/images/16de1600-91ac-4377-9aae-c3b66b300889.svg new file mode 100644 index 000000000..f5570c64b --- /dev/null +++ b/images/16de1600-91ac-4377-9aae-c3b66b300889.svg @@ -0,0 +1,828 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + LogEmitter + QuestionnaireManager + NetworkMessages + Experiment Events File + DebugEvents File + User Input + + + LogEmitter + UbiqObjectSpawner + + + LogEmitter + UbiqAudioChatManager + + + + + + + + + + + DebugEvent + DebugEvent + ExperimentEvent + + + + + EventsFilter + + + + + + + + + + + + + + + + + LogCollector + LogCollector + + diff --git a/images/2e34546b-a593-4acc-9f1b-9cdd53c199ca.png b/images/2e34546b-a593-4acc-9f1b-9cdd53c199ca.png new file mode 100644 index 000000000..cc346e1c1 Binary files /dev/null and b/images/2e34546b-a593-4acc-9f1b-9cdd53c199ca.png differ diff --git a/images/2eb639a7-a67e-4033-bcdb-9fa36c773fde.png b/images/2eb639a7-a67e-4033-bcdb-9fa36c773fde.png new file mode 100644 index 000000000..40326ff0d Binary files /dev/null and b/images/2eb639a7-a67e-4033-bcdb-9fa36c773fde.png differ diff --git a/images/324c9e07-ec87-4827-ba6c-74c64eb81d9b.png b/images/324c9e07-ec87-4827-ba6c-74c64eb81d9b.png new file mode 100644 index 000000000..89aae0217 Binary files /dev/null and b/images/324c9e07-ec87-4827-ba6c-74c64eb81d9b.png differ diff --git a/images/3866ced2-97a1-404a-8305-64d67b7ed1f7.png b/images/3866ced2-97a1-404a-8305-64d67b7ed1f7.png new file mode 100644 index 000000000..ae5435f7a Binary files /dev/null and b/images/3866ced2-97a1-404a-8305-64d67b7ed1f7.png differ diff --git a/images/3b19e567-c18b-47bb-809f-6276c22a64be.png b/images/3b19e567-c18b-47bb-809f-6276c22a64be.png new file mode 100644 index 000000000..14ba92f02 Binary files /dev/null and b/images/3b19e567-c18b-47bb-809f-6276c22a64be.png differ diff --git a/images/3faa3448-5b19-4959-8034-8352579c7171.png b/images/3faa3448-5b19-4959-8034-8352579c7171.png new file mode 100644 index 000000000..8d44230b8 Binary files /dev/null and b/images/3faa3448-5b19-4959-8034-8352579c7171.png differ diff --git a/images/4b2f6bd0-10de-426d-91f5-194aabf68e18.png b/images/4b2f6bd0-10de-426d-91f5-194aabf68e18.png new file mode 100644 index 000000000..4b499956d Binary files /dev/null and b/images/4b2f6bd0-10de-426d-91f5-194aabf68e18.png differ diff --git a/images/5041e3f3-bf2e-41df-837b-20546521b008.svg b/images/5041e3f3-bf2e-41df-837b-20546521b008.svg new file mode 100644 index 000000000..31cb4ecb8 --- /dev/null +++ b/images/5041e3f3-bf2e-41df-837b-20546521b008.svg @@ -0,0 +1,561 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + NetworkScene + RoomClient + RoomPeer + RoomServer + Room + + + + + + Socket + + Socket + + + Has One + Has Many + Has Many + + + + Creates + + + + Has One + + Has One + Messages + Peer + Server + + diff --git a/images/5b74ce51-93f7-4707-bce7-451d79186b06.png b/images/5b74ce51-93f7-4707-bce7-451d79186b06.png new file mode 100644 index 000000000..498c4a2fc Binary files /dev/null and b/images/5b74ce51-93f7-4707-bce7-451d79186b06.png differ diff --git a/images/5c0ae85b-b9b2-4595-b3ab-6b5809138426.png b/images/5c0ae85b-b9b2-4595-b3ab-6b5809138426.png new file mode 100644 index 000000000..96bbd63b7 Binary files /dev/null and b/images/5c0ae85b-b9b2-4595-b3ab-6b5809138426.png differ diff --git a/images/64a4c60f-42ec-4ecc-91fd-e02d90bcbd55.png b/images/64a4c60f-42ec-4ecc-91fd-e02d90bcbd55.png new file mode 100644 index 000000000..601477113 Binary files /dev/null and b/images/64a4c60f-42ec-4ecc-91fd-e02d90bcbd55.png differ diff --git a/images/672a7cae-bffd-4257-848d-fe1a54c5abe2.png b/images/672a7cae-bffd-4257-848d-fe1a54c5abe2.png new file mode 100644 index 000000000..18b7da768 Binary files /dev/null and b/images/672a7cae-bffd-4257-848d-fe1a54c5abe2.png differ diff --git a/images/6d75e6c4-a29b-458e-909c-847b7da0796d.png b/images/6d75e6c4-a29b-458e-909c-847b7da0796d.png new file mode 100644 index 000000000..0dd692e68 Binary files /dev/null and b/images/6d75e6c4-a29b-458e-909c-847b7da0796d.png differ diff --git a/images/740061ba-7bfe-4832-9c97-d75e85b9e26c.png b/images/740061ba-7bfe-4832-9c97-d75e85b9e26c.png new file mode 100644 index 000000000..9970e362d Binary files /dev/null and b/images/740061ba-7bfe-4832-9c97-d75e85b9e26c.png differ diff --git a/images/741ad326-3211-405f-a61d-36cefe03addb.svg b/images/741ad326-3211-405f-a61d-36cefe03addb.svg new file mode 100644 index 000000000..6282f6435 --- /dev/null +++ b/images/741ad326-3211-405f-a61d-36cefe03addb.svg @@ -0,0 +1,566 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + LogCollector + Forward Mode + + + + LogCollector + Forward Mode + + + + LogCollector + Forward Mode + + + + LogCollector + Write Mode + + + + LogEmitter + + + + LogEmitter + + + + LogEmitter + + + + LogEmitter + + + + LogEmitter + + + + + + + + + + + + + + + + + LogEmitter + + + + diff --git a/images/7bf0b8de-037a-4f6e-a038-60d80577b575.png b/images/7bf0b8de-037a-4f6e-a038-60d80577b575.png new file mode 100644 index 000000000..f7657ab3f Binary files /dev/null and b/images/7bf0b8de-037a-4f6e-a038-60d80577b575.png differ diff --git a/images/81afbd3b-42c1-419f-998c-1856f3fb36b3.png b/images/81afbd3b-42c1-419f-998c-1856f3fb36b3.png new file mode 100644 index 000000000..cd557326a Binary files /dev/null and b/images/81afbd3b-42c1-419f-998c-1856f3fb36b3.png differ diff --git a/images/8a412806-c63d-4fb1-9a65-0ec1876ff901.svg b/images/8a412806-c63d-4fb1-9a65-0ec1876ff901.svg new file mode 100644 index 000000000..8d0aa54db --- /dev/null +++ b/images/8a412806-c63d-4fb1-9a65-0ec1876ff901.svg @@ -0,0 +1,566 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + LogCollector + Forward Mode + + + + LogCollector + Forward Mode + + + + LogCollector + Forward Mode + + + + LogCollector + Write Mode + + + + LogEmitter + + + + LogEmitter + + + + LogEmitter + + + + LogEmitter + + + + LogEmitter + + + + + + + + + + + + + + + + + LogEmitter + + + + diff --git a/images/8b7c15e1-9113-448f-ac49-f95893e584ee.png b/images/8b7c15e1-9113-448f-ac49-f95893e584ee.png new file mode 100644 index 000000000..cf488dff2 Binary files /dev/null and b/images/8b7c15e1-9113-448f-ac49-f95893e584ee.png differ diff --git a/images/8e10788d-eca6-4009-b673-d4c34ad7f6a0.png b/images/8e10788d-eca6-4009-b673-d4c34ad7f6a0.png new file mode 100644 index 000000000..8d5673480 Binary files /dev/null and b/images/8e10788d-eca6-4009-b673-d4c34ad7f6a0.png differ diff --git a/images/8e9dfe00-e9d5-4980-8373-e264c7f4c1e1.png b/images/8e9dfe00-e9d5-4980-8373-e264c7f4c1e1.png new file mode 100644 index 000000000..aede27ab0 Binary files /dev/null and b/images/8e9dfe00-e9d5-4980-8373-e264c7f4c1e1.png differ diff --git a/images/90c6e96e-781b-4205-8b03-5ad4c5d29150.svg b/images/90c6e96e-781b-4205-8b03-5ad4c5d29150.svg new file mode 100644 index 000000000..72b06b3e9 --- /dev/null +++ b/images/90c6e96e-781b-4205-8b03-5ad4c5d29150.svg @@ -0,0 +1,562 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + LogCollector + Forward Mode + + + + LogCollector + Forward Mode + + + + + LogCollector + Forward Mode + + + + LogCollector + Forward Mode + + + + LogCollector + Write Mode + + + + LogEmitter + + + + LogEmitter + + + + LogEmitter + + + + LogEmitter + + + + LogEmitter + + + + + + + + + + + + + + + + + diff --git a/images/91480060-110c-4e2f-a379-c06c480ea606.png b/images/91480060-110c-4e2f-a379-c06c480ea606.png new file mode 100644 index 000000000..53492fe51 Binary files /dev/null and b/images/91480060-110c-4e2f-a379-c06c480ea606.png differ diff --git a/images/96e766f2-3d71-429c-8e53-0abce4528621.png b/images/96e766f2-3d71-429c-8e53-0abce4528621.png new file mode 100644 index 000000000..ac2b230af Binary files /dev/null and b/images/96e766f2-3d71-429c-8e53-0abce4528621.png differ diff --git a/images/9e093fbb-d84f-4f48-ae00-0a7019975d79.svg b/images/9e093fbb-d84f-4f48-ae00-0a7019975d79.svg new file mode 100644 index 000000000..13db93d8e --- /dev/null +++ b/images/9e093fbb-d84f-4f48-ae00-0a7019975d79.svg @@ -0,0 +1,114 @@ + + + diff --git a/images/a2cf83c8-432e-4a7d-bcba-1bcd8afd0892.svg b/images/a2cf83c8-432e-4a7d-bcba-1bcd8afd0892.svg new file mode 100644 index 000000000..b52b447fc --- /dev/null +++ b/images/a2cf83c8-432e-4a7d-bcba-1bcd8afd0892.svg @@ -0,0 +1,344 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + LogCollector + + + + + + Participant PC + + + LogCollector + + + + + + + + File + + + + + Experimentor PC + Writing LogCollector + + + diff --git a/images/b452b4c9-e186-415c-b026-a79cc24f9151.png b/images/b452b4c9-e186-415c-b026-a79cc24f9151.png new file mode 100644 index 000000000..77293b305 Binary files /dev/null and b/images/b452b4c9-e186-415c-b026-a79cc24f9151.png differ diff --git a/images/b5c1f706-b9dc-4265-ac50-396e0026ee66.png b/images/b5c1f706-b9dc-4265-ac50-396e0026ee66.png new file mode 100644 index 000000000..a1b9b4ed2 Binary files /dev/null and b/images/b5c1f706-b9dc-4265-ac50-396e0026ee66.png differ diff --git a/images/bb403b80-4eff-444f-8e51-cc8659bd9803.png b/images/bb403b80-4eff-444f-8e51-cc8659bd9803.png new file mode 100644 index 000000000..af9c1c712 Binary files /dev/null and b/images/bb403b80-4eff-444f-8e51-cc8659bd9803.png differ diff --git a/images/bd892032-38e4-4ddd-bc38-0d10437cdcb6.png b/images/bd892032-38e4-4ddd-bc38-0d10437cdcb6.png new file mode 100644 index 000000000..96e9c36c2 Binary files /dev/null and b/images/bd892032-38e4-4ddd-bc38-0d10437cdcb6.png differ diff --git a/images/d046700a-a462-4ef0-a7df-b85db38dc688.png b/images/d046700a-a462-4ef0-a7df-b85db38dc688.png new file mode 100644 index 000000000..b47175817 Binary files /dev/null and b/images/d046700a-a462-4ef0-a7df-b85db38dc688.png differ diff --git a/images/d713fe99-caba-47ba-805d-11a4da98c6d6.svg b/images/d713fe99-caba-47ba-805d-11a4da98c6d6.svg new file mode 100644 index 000000000..c9e3a3e3f --- /dev/null +++ b/images/d713fe99-caba-47ba-805d-11a4da98c6d6.svg @@ -0,0 +1,753 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + NetworkScene + PlayerController + + Application +Code + User Actions + Network + + + + + + + NetworkScene + Bot + + Application +Code + Network + + Regular Peer + Bot Peer + + + + + + + + diff --git a/images/d7937ef2-069b-40e2-b596-1ceccd749a24.png b/images/d7937ef2-069b-40e2-b596-1ceccd749a24.png new file mode 100644 index 000000000..cb722561e Binary files /dev/null and b/images/d7937ef2-069b-40e2-b596-1ceccd749a24.png differ diff --git a/images/e13a146f-7f90-486d-8131-a47d44f8c87c.png b/images/e13a146f-7f90-486d-8131-a47d44f8c87c.png new file mode 100644 index 000000000..af55b4af4 Binary files /dev/null and b/images/e13a146f-7f90-486d-8131-a47d44f8c87c.png differ diff --git a/images/e1457011-ec82-49c7-8993-c1283810df94.png b/images/e1457011-ec82-49c7-8993-c1283810df94.png new file mode 100644 index 000000000..be8e6d1c7 Binary files /dev/null and b/images/e1457011-ec82-49c7-8993-c1283810df94.png differ diff --git a/images/e8cae779-d00b-47ac-81f2-28f14e6b8fd8.png b/images/e8cae779-d00b-47ac-81f2-28f14e6b8fd8.png new file mode 100644 index 000000000..ff1f9617d Binary files /dev/null and b/images/e8cae779-d00b-47ac-81f2-28f14e6b8fd8.png differ diff --git a/images/ea68f5a9-95c3-46f6-97ef-b975a6c6108a.png b/images/ea68f5a9-95c3-46f6-97ef-b975a6c6108a.png new file mode 100644 index 000000000..182fbe3b2 Binary files /dev/null and b/images/ea68f5a9-95c3-46f6-97ef-b975a6c6108a.png differ diff --git a/images/ea6a9af2-8ec6-43c2-977d-d2cee821cfca.png b/images/ea6a9af2-8ec6-43c2-977d-d2cee821cfca.png new file mode 100644 index 000000000..b47185ae2 Binary files /dev/null and b/images/ea6a9af2-8ec6-43c2-977d-d2cee821cfca.png differ diff --git a/images/eb7cf431-5c49-41cb-b058-61fb2d06162c.png b/images/eb7cf431-5c49-41cb-b058-61fb2d06162c.png new file mode 100644 index 000000000..2571eaabe Binary files /dev/null and b/images/eb7cf431-5c49-41cb-b058-61fb2d06162c.png differ diff --git a/images/f5bb3981-78f3-49f2-b606-5359674d8c9d.png b/images/f5bb3981-78f3-49f2-b606-5359674d8c9d.png new file mode 100644 index 000000000..1c238a71a Binary files /dev/null and b/images/f5bb3981-78f3-49f2-b606-5359674d8c9d.png differ diff --git a/images/f9a34f6f-d584-41cc-8b07-2c02c9b17602.png b/images/f9a34f6f-d584-41cc-8b07-2c02c9b17602.png new file mode 100644 index 000000000..9da50691b Binary files /dev/null and b/images/f9a34f6f-d584-41cc-8b07-2c02c9b17602.png differ diff --git a/img/favicon.ico b/img/favicon.ico new file mode 100644 index 000000000..e85006a3c Binary files /dev/null and b/img/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 000000000..0d9dfd124 --- /dev/null +++ b/index.html @@ -0,0 +1,228 @@ + + + + + + + + Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Welcome to Ubiq

+

Picture of Avatars Waving

+

Ubiq is a Unity networking library, for research, teaching and development.

+

Ubiq is built and maintained by the Virtual Environments and Computer Graphics group at University College London.

+

Ubiq is 100% free and open source.

+

Features

+

Ubiq's goal is to enable your networked project. It includes message passing, room management, rendezvous and matchmaking, object spawning, shared binary blobs, multiple synchronisation models, lighweight XR interaction examples, customisable avatars and voice chat across Windows, Linux, Android, MacOS, and Javascript running in the browser.

+

For Researchers

+

Instructions for setting up your own server are included. Ubiq does not rely on any third-party services, making it GDPR-safe for your experiments.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + + Next » + + +
+ + + + + + + + + + + diff --git a/js/html5shiv.min.js b/js/html5shiv.min.js new file mode 100644 index 000000000..1a01c94ba --- /dev/null +++ b/js/html5shiv.min.js @@ -0,0 +1,4 @@ +/** +* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed +*/ +!function(a,b){function c(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function d(){var a=t.elements;return"string"==typeof a?a.split(" "):a}function e(a,b){var c=t.elements;"string"!=typeof c&&(c=c.join(" ")),"string"!=typeof a&&(a=a.join(" ")),t.elements=c+" "+a,j(b)}function f(a){var b=s[a[q]];return b||(b={},r++,a[q]=r,s[r]=b),b}function g(a,c,d){if(c||(c=b),l)return c.createElement(a);d||(d=f(c));var e;return e=d.cache[a]?d.cache[a].cloneNode():p.test(a)?(d.cache[a]=d.createElem(a)).cloneNode():d.createElem(a),!e.canHaveChildren||o.test(a)||e.tagUrn?e:d.frag.appendChild(e)}function h(a,c){if(a||(a=b),l)return a.createDocumentFragment();c=c||f(a);for(var e=c.frag.cloneNode(),g=0,h=d(),i=h.length;i>g;g++)e.createElement(h[g]);return e}function i(a,b){b.cache||(b.cache={},b.createElem=a.createElement,b.createFrag=a.createDocumentFragment,b.frag=b.createFrag()),a.createElement=function(c){return t.shivMethods?g(c,a,b):b.createElem(c)},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+d().join().replace(/[\w\-:]+/g,function(a){return b.createElem(a),b.frag.createElement(a),'c("'+a+'")'})+");return n}")(t,b.frag)}function j(a){a||(a=b);var d=f(a);return!t.shivCSS||k||d.hasCSS||(d.hasCSS=!!c(a,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),l||i(a,d),a}var k,l,m="3.7.3",n=a.html5||{},o=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,p=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,q="_html5shiv",r=0,s={};!function(){try{var a=b.createElement("a");a.innerHTML="",k="hidden"in a,l=1==a.childNodes.length||function(){b.createElement("a");var a=b.createDocumentFragment();return"undefined"==typeof a.cloneNode||"undefined"==typeof a.createDocumentFragment||"undefined"==typeof a.createElement}()}catch(c){k=!0,l=!0}}();var t={elements:n.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:n.shivCSS!==!1,supportsUnknownElements:l,shivMethods:n.shivMethods!==!1,type:"default",shivDocument:j,createElement:g,createDocumentFragment:h,addElements:e};a.html5=t,j(b),"object"==typeof module&&module.exports&&(module.exports=t)}("undefined"!=typeof window?window:this,document); diff --git a/js/jquery-3.6.0.min.js b/js/jquery-3.6.0.min.js new file mode 100644 index 000000000..c4c6022f2 --- /dev/null +++ b/js/jquery-3.6.0.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t + + + + + + + Leaving and Joining - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + + +
  • +
  • +
+
+
+
+
+ +

Leaving and Joining

+

When a RoomClient starts it is automatically a member of an empty, unidentified, room.

+

A RoomClient can create a new room or join an existing one at any time. Joining or leaving a room involes a room change. Changing the room is the same whether the client is going to or from an empty room, or between non-empty rooms.

+

When the room changes RoomClient will emit a number of events, in order:

+
    +
  1. OnPeerRemoved for any peers that go out of scope
  2. +
  3. OnPeerAdded for any peers that come into scope
  4. +
  5. OnPeerUpdated for any peers that are in scope but whose properties have changed
  6. +
  7. OnJoinedRoom with the new room
  8. +
  9. OnRoomUpdated with the new room
  10. +
+

Whether a Peer is in scope means whether or not it is available to current Peer. If two Peers moved to another room at the same time, they would remain in scope because the Peers themselves remain in the same rooms, even though that room has changed. On the other hand, if a Peer joins a different Room, it will go out of scope, as it is no longer in the same room, even though it may still be connected to the server.

+

The purpose of rooms is to facilitate message exchanging between specific sets of peers. Most Components should use the Peer events to create, destroy and update objects. The Room events are typically used when code needs to control room membership, for example the UI panels for joining rooms.

+

There is no such thing as leaving a room; underneath, leaving a Room means joining a new, empty Room.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/localloopback/index.html b/localloopback/index.html new file mode 100644 index 000000000..b1462f84b --- /dev/null +++ b/localloopback/index.html @@ -0,0 +1,228 @@ + + + + + + + + Testing with Local Loopback - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Testing Networked Objects

+

A Local Loopback scene is included in the Ubiq Samples. Local loopback is helpful for testing your networked objects in only one copy of Unity. The scene contains two clients that join a new room when you enter Play Mode.

+

+

The scene is in the Samples folder (Start Here - Local Loopback). Click the play button to get started.

+

In the Scene View you should see two Avatars. One avatar is you - controlled via the Game View - and the other is Bot, which joins the other NetworkScene.

+

+

Each environment is a separate Ubiq Peer. The script attached to the Local Loopback GameObject tells both instances to join the same Room. You can see on your in-game display the Join Code for this Room. Try and join it with a third Ubiq client and see what happens!

+

Forests

+

Objects, such as Avatars and Spawned Items, are associated with a NetworkScene. They find their Network Scene by searching the Scene Graph for the closest one.

+

The Local Loopback scene has two Network Scenes, each under a top level GameObject, called a Forest. These GameObjects separate the Unity scene into branches, where all Components in one branch will find that branch's Network Scene.

+

You can add more Forests to the scene to create more Peers.

+

Only one Peer can be controlled by the PlayerController at any time however, since Unity only supports one set of Input Devices. The other Peers are controlled by Bots.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/messaging/index.html b/messaging/index.html new file mode 100644 index 000000000..727e8474c --- /dev/null +++ b/messaging/index.html @@ -0,0 +1,234 @@ + + + + + + + + Introduction - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + + +
  • +
  • +
+
+
+
+
+ +

Introduction

+

Ubiq is built around exchanging exchanges between instances of networked Components. A message sent by one Component is received by all other similar Components (multicasting).

+

This logical behaviour is decoupled from the actual connection architecture, which is built around unicast TCP connections intended to operate over the internet. Messages are variable size with binary payloads. All messages passed by Ubiq have the same header.

+

The Medium is the Message

+

Ubiq delivers individual messages between instances of networked objects (most commonly Unity Components). How messages get from Component A to Component B is the responsiblity of the Messaging Layer, and is abstracted from the developer.

+

Network Components only recieve messages addressed to them directly, so they can rely on knowing the type of the message by virtue of having recieved it.

+

The expected programming model is that Components implement send and recieve functionality in the same script, which is also where the format is defined. By the time a message reaches a Component, it is received as the exact raw sequence of bytes sent by the Component's counterpart. Individual components choose the best serialisation method and transmission frequency for their use case.

+

Scene Graph as a Bus

+

Ubiq closely matches Unity's programming model. Since user code is placed in Components in Unity, networked objects in Ubiq are also Unity Components.

+

Each Component has a Network Id which uniquely identifies that instance on the Peer. The Id is shared by equivalent instances on other Peers.

+

All Components are associated with one NetworkScene. This is the networking equivalent to the root of the scene graph. Components find their Network Scene based on the distance through the scene graph to the closest instance of a NetworkScene Component.

+

Fan-Out at the Network Layer

+

When a Component transmits a message, it is recieved by every other Component with the same address (Network Id). This is the case regardless of how many instances there are with the same combination. Fanning out - or multicasting - the message is done at the network layer. Individual Components don't know how many counterparts they have.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/mkdocs/index.html b/mkdocs/index.html new file mode 100644 index 000000000..6a67172be --- /dev/null +++ b/mkdocs/index.html @@ -0,0 +1,221 @@ + + + + + + + + Meta - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

MkDocs Commands

+

This page is for the Ubiq developers.

+

This documentation is generated with MkDocs. For full documentation visit mkdocs.org.

+

To keep linking between pages simple, try to keep all MD files in the docs/ directory. Images should be placed in images/ and all filenames should be GUIDS. Please delete images if you remove a reference to them. The exception is reusable assets such as logos, which have human-readable names.

+

There is a special branch of Ubiq ("Documentation") which can be used to store Scenes and other assets that were set up for screenshots.

+
    +
  • mkdocs new [dir-name] - Create a new project.
  • +
  • mkdocs serve - Start the live-reloading docs server.
  • +
  • mkdocs build - Build the documentation site.
  • +
  • mkdocs -h - Print help message and exit.
  • +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + +
+ + + + + + + + + diff --git a/networkids/index.html b/networkids/index.html new file mode 100644 index 000000000..3705b60c9 --- /dev/null +++ b/networkids/index.html @@ -0,0 +1,246 @@ + + + + + + + + Network Ids - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + + +
  • +
  • +
+
+
+
+
+ +

Network Ids

+

Network Ids are analogous to the instance Id of a Unity Component. This is how Component instances address eachother.

+

A Component type may exist on one or more GameObjects, or it may exist multiple times on one GameObject. Each of these instances within the Unity Scene or process will have a different instance Id, and by default a different network Id.

+

So, if there are two copies of a Prefab in a Scene, they will talk to their respective copies, and only their respective copies, on other Peers.

+

Default Network Id

+

The default behaviour of Ubiq is to generate a Network Id for a Component when it calls Register(). The Id is based on the Component's address is in the scene graph. This is done predominantly by name.

+

If you have two scenes with the same hierarchy, and two GameObjects with the same name, and same networked Components, those Components will end up with the same Id. This is true even if the scenes are different Unity scenes, or if the scenes are on different devices.

+

There is some flexibility. For example, if you had two GameObjects with different names, their order in the scene graph could be inverted and the addressing would still work. You couldn't invert the order of GameObjects with the same name however, or put them in different branches of the same scene graph.

+

Network Id Gizmo

+

If you want to see the addresses of your Networked Objects, the NetworkIdGizmo Component can be used to show them at Runtime in the Scene View.

+

+

In the image above, NetworkIdGizmo is attached to an Avatar. The Avatar has one networked Component - ThreePointTrackedAvatar - with an Id of 8eef84fa-5d1ec5ee.

+

Specifying a specific Network Id

+

The default behaviour is intended to be used where one Unity Scene is created for an application, and that same scene is used by all Peers. In this case, as there is only one scene the addresses between all Peers' Components will always be the same and there is no need to worry about keeping them synchronised.

+

However, there are many cases where you may want to directly control the Network Id. For example, you may want to pre-share a Network Id to create a service. You may want to send messages from one Component to a Component with a different Network Id.

+

In these cases you can call NetworkScene::Register(MonoBehaviour, NetworkId). This overload will register the Component with the specified Network Id, instead of generating one automatically, allowing you to use any communication pattern you like for your objects.

+

Generating Ids

+

You can generate a new, unique network Id by calling NetworkId::Unique().

+

Network Context & Send

+

Register returns a NetworkContext object with a Send method. This method sends messages to the same Id as stored in the context (the one registered to the networked Component).

+

If you want to send messages to a different Network Id, you can call NetworkScene::Send() directly, which takes a message and a Network Id to send that message to.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/networkidtype/index.html b/networkidtype/index.html new file mode 100644 index 000000000..5a01b368c --- /dev/null +++ b/networkidtype/index.html @@ -0,0 +1,233 @@ + + + + + + + + Types - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + + +
  • +
  • +
+
+
+
+
+ +

Network and Component Types

+

Network Ids

+

Network Ids are 64-bit identifiers. They are represented by the Ubiq.Messaging.NetworkId structure in C# and the Messaging.NetworkId class in Javascript.

+

In C# Ids are value types, and the equality operators are overridden. In Javascript they are reference types and must be explicitly compared using the static NetworkId.Compare method.

+
NetworkId.Compare(message.objectId, server.objectId)
+
+

Internally, the types are represented by two 32 bit integers. This is an implementation detail and should not be relied upon. The reason for using two 32 bit integers rather than one 64 bit long, is that Javascript only supports 53 bit integers.

+

Binary vs Json

+

In Javascript, NetworkIds must be handled in both their binary and Json forms. This is because Ubiq messages include the NetworkId in their binary header, while some Javascript services, such as the Room Server, accept NetworkIds as arguments.

+

For example, a Json message would arrive containing the Id of an object that the Javascript code should send a message to. The Javascript code will need to convert that into a binary representation in order to build the header.

+

Binary NetworkIds are converted to NetworkId class instances by the Message wrapper. From this point on any Javascript code can work with the object in its Json representation.

+

NetworkId instances and generic Json objects representing a network Id can interoperate.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/nexus/index.html b/nexus/index.html new file mode 100644 index 000000000..c434e230f --- /dev/null +++ b/nexus/index.html @@ -0,0 +1,302 @@ + + + + + + + + Nexus - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Nexus

+

Overview

+

The UCL VECG hosts multiple instances of the rendezvous server on nexus.cs.ucl.ac.uk. Different branches of this repository are checked on nexus and run on different ports.

+

The checkouts are in /home/node and follow the format ubiq-[branch name].

+

Currently ubiq-master is running on 8010. This is the primary, public server.

+

It is expected and encouraged that feature branches are created, run on nexus temporarily for development, then removed when no longer needed.

+

The following sections describe how Nexus is maintained by the VECG team. You do not need to follow this pattern to maintain your own server, but it may be instructive.

+

Administration

+

Access to nexus.cs.ucl.ac.uk is via SSH.

+

The nodejs process is managed via pm2. The relevant commands are:

+
    +
  • pm2 list (Shows running processes)
  • +
  • pm2 log (Shows the logs)
  • +
  • pm2 flush (Clears the logs)
  • +
  • pm2 restart (Restarts the app, e.g. after an update)
  • +
+

pm2 is responsible for restarting the nodejs process after a server restart. To save the state of the tasks that it will try to restore, give the command pm2 save.

+

nodejs and pm2 run under the node account. All maintenance should be performed as node. The username/password is node/node. It is not possible to SSH directly as node; login with your CS credentials, then change user with su (i.e. $ su node).

+

All VECG members who request access will be given sudo permission. All members will be collaborators on the Github repository. Any member can add new members.

+

Git

+

The node account has been given access to the GitHub repository through a Deploy Key. This is a single-use SSH key associated with the repository.

+

New

+

When cloning a new copy for a branch, you must specify the folder as git will always clone into the repository name by default. For example: +git clone --depth 1 git@github.com:UCL-VR/ubiq.git ubiq-master

+

You can specify the branch name for the clone command (git clone --depth 1 --branch master git@github.com:UCL-VR/ubiq.git ubiq-master), or checkout the appropriate branch after.

+

The --depth 1 command downloads only the HEAD, which is all that is needed to run the server.

+

After cloning, navigate to ~/ubiq-[branch name]/Node/ and issue the commands:

+
    +
  1. npm install
  2. +
  3. pm2 start npm -- name "ubiq-[branch name]" -- start
  4. +
+

The first installs the nodejs dependencies and the second creates the pm2 job with a unique name to identify the instance.

+

Updating

+

To update a checkout, enter the repository and issue git pull. node cannot write to the repository.

+

Firewall

+

Be aware when creating your own clones that you will need to open your chosen ports on the local firewall and reload it.

+

To see the firewall rules on CentOS, give the command,

+
 sudo iptables -L -n -v --line-n
+
+

The -n argument shows the port numbers, rather than showing the names of typical services that run on them. -v shows the interface, and --line-n shows line numbers, which will be important when adding rules.

+

To add a new rule give the commands,

+
sudo iptables -I INPUT 5 -p tcp --dport 8010 -j ACCEPT
+sudo service iptables save
+
+

The number after INPUT indicates the line that the rule should be added at. The ACCEPT rule must come before the catch-all REJECT rule. For example, the REJECT rule is on line 11 below and will reject all packets not matching any of the rules above it.

+
1    ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            state RELATED,ESTABLISHED
+2    ACCEPT     icmp --  0.0.0.0/0            0.0.0.0/0
+3    ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0
+4    ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            state NEW tcp dpt:22
+5    ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:8006
+6    ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:8005
+7    ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:8004
+8    ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:8003
+9    ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:8002
+10   ACCEPT     tcp  --  0.0.0.0/0            0.0.0.0/0            tcp dpt:8001
+11   REJECT     all  --  0.0.0.0/0            0.0.0.0/0            reject-with icmp-host-prohibited
+
+

The iptables save is for the iptables-service, which is installed on Nexus, and allows saving and automatically reloading firewall rules on reboot.

+

Be aware also that opening the ports locally will not make them available on the public internet, but only via the CS VPN. Nexus has range 8000-8020 open on the CS and ISD firewalls.

+

Legacy Versions

+

When breaking changes are made, legacy versions of the server will be checked out with their last supported version number, e.g. ubiq-0.0.6, and instances of these will be left running. The ports that they listen on will be incremented with each breaking change.

+

Old clients will therefore continue to work, though old and new versions cannot communicate with eachother.

+

Not all versions include breaking changes, so the sequence of legacy versions running is not continuous. The latest version is always ubiq-master.

+

Currently ubiq-0.0.6 is running on 8001.

+

References

+
    +
  • https://upcloud.com/community/tutorials/configure-iptables-centos/
  • +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/packaging/index.html b/packaging/index.html new file mode 100644 index 000000000..27cad5eb8 --- /dev/null +++ b/packaging/index.html @@ -0,0 +1,244 @@ + + + + + + + + Packaging - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + +
  • +
  • +
+
+
+
+
+ +

Packaging

+

Overview

+

The Unity side of the project can be built as a package for the Unity Package Manager.

+

Releases

+

Release numbers use SemVer. The version is to be put in the filename and the package.json file.

+

The intention is that each release corresponds to a commit. To this end if you are making a new release version of the package:

+
    +
  1. Make sure you have no modified files (check your git status - the script does this for you)
  2. +
  3. Add a tag on your current commit with the release number, push the tag to the origin repo
  4. +
+

Building (Script)

+

There's a simple build script included in the project. It's integrated with the Unity Editor. Access it through the taskbar:

+

Ubik-dev -> Pack for Unity Package Manager

+

Building (Manual)

+

Building the package manually is quite painless.

+
    +
  1. Check git status does not indicate any modified files
  2. +
  3. Make a new folder and copy over the Editor, Runtime and Samples folders and the package.json and package.json.meta files
  4. +
  5. Rename the Samples folder to Samples~ (this stops Unity importing it into the main package)
  6. +
  7. Zip it!
  8. +
  9. Name the zipped file ubik-{versionnumber}.zip
  10. +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/reservedids/index.html b/reservedids/index.html new file mode 100644 index 000000000..f7ad468b4 --- /dev/null +++ b/reservedids/index.html @@ -0,0 +1,223 @@ + + + + + + + + Reserved IDs - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + + + +
  • +
  • +
+
+
+
+
+ +

Common Ids

+

A number of Ids are pre-assigned to common objects in Ubiq. These are listed below.

+

Network Ids

+
    +
  • RoomServer: 0000-0000-0000-0001
  • +
+ +
+
+ +
+
+ +
+ +
+ +
+ + + + « Previous + + + Next » + + +
+ + + + + + + + + diff --git a/roomserverversioning/index.html b/roomserverversioning/index.html new file mode 100644 index 000000000..ec0ee1dfa --- /dev/null +++ b/roomserverversioning/index.html @@ -0,0 +1,211 @@ + + + + + + + + Versioning - Ubiq Docs + + + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ +

Versioning

+

There is no such thing as an "authoritative" server in Ubiq. Components such as the Room Server implement services to fulfil a particular goal. They behave as traditional servers through some concepts:

+
    +
  1. They have pre-defined Ids
  2. +
  3. In some cases, such as the actual RoomServer Node App, they process messages themselves and change the messaing behaviour.
  4. +
+

Such services use pre-defined Ids and can make assumptions about how they will be used, but always at least assume that both themselves and their users are in a Ubiq mesh. Components like the RoomClient will bootstrap this by creating those initial connections.

+

Each service then is responsible for maintaing, or not, compatability and version control.

+ +
+
+ +
+
+ +
+ +
+ +
+ + + + + +
+ + + + + + + + + diff --git a/search.html b/search.html new file mode 100644 index 000000000..8124d6bbf --- /dev/null +++ b/search.html @@ -0,0 +1,207 @@ + + + + + + + + Ubiq Docs + + + + + + + + + + + +
+ + +
+ +
+
+
    +
  • +
  • +
  • +
+
+
+
+
+ + +

Search Results

+ + + +
+ Searching... +
+ + +
+
+ +
+
+ +
+ +
+ +
+ + + + + +
+ + + + + + + + + diff --git a/search/lunr.js b/search/lunr.js new file mode 100644 index 000000000..aca0a167f --- /dev/null +++ b/search/lunr.js @@ -0,0 +1,3475 @@ +/** + * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9 + * Copyright (C) 2020 Oliver Nightingale + * @license MIT + */ + +;(function(){ + +/** + * A convenience function for configuring and constructing + * a new lunr Index. + * + * A lunr.Builder instance is created and the pipeline setup + * with a trimmer, stop word filter and stemmer. + * + * This builder object is yielded to the configuration function + * that is passed as a parameter, allowing the list of fields + * and other builder parameters to be customised. + * + * All documents _must_ be added within the passed config function. + * + * @example + * var idx = lunr(function () { + * this.field('title') + * this.field('body') + * this.ref('id') + * + * documents.forEach(function (doc) { + * this.add(doc) + * }, this) + * }) + * + * @see {@link lunr.Builder} + * @see {@link lunr.Pipeline} + * @see {@link lunr.trimmer} + * @see {@link lunr.stopWordFilter} + * @see {@link lunr.stemmer} + * @namespace {function} lunr + */ +var lunr = function (config) { + var builder = new lunr.Builder + + builder.pipeline.add( + lunr.trimmer, + lunr.stopWordFilter, + lunr.stemmer + ) + + builder.searchPipeline.add( + lunr.stemmer + ) + + config.call(builder, builder) + return builder.build() +} + +lunr.version = "2.3.9" +/*! + * lunr.utils + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A namespace containing utils for the rest of the lunr library + * @namespace lunr.utils + */ +lunr.utils = {} + +/** + * Print a warning message to the console. + * + * @param {String} message The message to be printed. + * @memberOf lunr.utils + * @function + */ +lunr.utils.warn = (function (global) { + /* eslint-disable no-console */ + return function (message) { + if (global.console && console.warn) { + console.warn(message) + } + } + /* eslint-enable no-console */ +})(this) + +/** + * Convert an object to a string. + * + * In the case of `null` and `undefined` the function returns + * the empty string, in all other cases the result of calling + * `toString` on the passed object is returned. + * + * @param {Any} obj The object to convert to a string. + * @return {String} string representation of the passed object. + * @memberOf lunr.utils + */ +lunr.utils.asString = function (obj) { + if (obj === void 0 || obj === null) { + return "" + } else { + return obj.toString() + } +} + +/** + * Clones an object. + * + * Will create a copy of an existing object such that any mutations + * on the copy cannot affect the original. + * + * Only shallow objects are supported, passing a nested object to this + * function will cause a TypeError. + * + * Objects with primitives, and arrays of primitives are supported. + * + * @param {Object} obj The object to clone. + * @return {Object} a clone of the passed object. + * @throws {TypeError} when a nested object is passed. + * @memberOf Utils + */ +lunr.utils.clone = function (obj) { + if (obj === null || obj === undefined) { + return obj + } + + var clone = Object.create(null), + keys = Object.keys(obj) + + for (var i = 0; i < keys.length; i++) { + var key = keys[i], + val = obj[key] + + if (Array.isArray(val)) { + clone[key] = val.slice() + continue + } + + if (typeof val === 'string' || + typeof val === 'number' || + typeof val === 'boolean') { + clone[key] = val + continue + } + + throw new TypeError("clone is not deep and does not support nested objects") + } + + return clone +} +lunr.FieldRef = function (docRef, fieldName, stringValue) { + this.docRef = docRef + this.fieldName = fieldName + this._stringValue = stringValue +} + +lunr.FieldRef.joiner = "/" + +lunr.FieldRef.fromString = function (s) { + var n = s.indexOf(lunr.FieldRef.joiner) + + if (n === -1) { + throw "malformed field ref string" + } + + var fieldRef = s.slice(0, n), + docRef = s.slice(n + 1) + + return new lunr.FieldRef (docRef, fieldRef, s) +} + +lunr.FieldRef.prototype.toString = function () { + if (this._stringValue == undefined) { + this._stringValue = this.fieldName + lunr.FieldRef.joiner + this.docRef + } + + return this._stringValue +} +/*! + * lunr.Set + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A lunr set. + * + * @constructor + */ +lunr.Set = function (elements) { + this.elements = Object.create(null) + + if (elements) { + this.length = elements.length + + for (var i = 0; i < this.length; i++) { + this.elements[elements[i]] = true + } + } else { + this.length = 0 + } +} + +/** + * A complete set that contains all elements. + * + * @static + * @readonly + * @type {lunr.Set} + */ +lunr.Set.complete = { + intersect: function (other) { + return other + }, + + union: function () { + return this + }, + + contains: function () { + return true + } +} + +/** + * An empty set that contains no elements. + * + * @static + * @readonly + * @type {lunr.Set} + */ +lunr.Set.empty = { + intersect: function () { + return this + }, + + union: function (other) { + return other + }, + + contains: function () { + return false + } +} + +/** + * Returns true if this set contains the specified object. + * + * @param {object} object - Object whose presence in this set is to be tested. + * @returns {boolean} - True if this set contains the specified object. + */ +lunr.Set.prototype.contains = function (object) { + return !!this.elements[object] +} + +/** + * Returns a new set containing only the elements that are present in both + * this set and the specified set. + * + * @param {lunr.Set} other - set to intersect with this set. + * @returns {lunr.Set} a new set that is the intersection of this and the specified set. + */ + +lunr.Set.prototype.intersect = function (other) { + var a, b, elements, intersection = [] + + if (other === lunr.Set.complete) { + return this + } + + if (other === lunr.Set.empty) { + return other + } + + if (this.length < other.length) { + a = this + b = other + } else { + a = other + b = this + } + + elements = Object.keys(a.elements) + + for (var i = 0; i < elements.length; i++) { + var element = elements[i] + if (element in b.elements) { + intersection.push(element) + } + } + + return new lunr.Set (intersection) +} + +/** + * Returns a new set combining the elements of this and the specified set. + * + * @param {lunr.Set} other - set to union with this set. + * @return {lunr.Set} a new set that is the union of this and the specified set. + */ + +lunr.Set.prototype.union = function (other) { + if (other === lunr.Set.complete) { + return lunr.Set.complete + } + + if (other === lunr.Set.empty) { + return this + } + + return new lunr.Set(Object.keys(this.elements).concat(Object.keys(other.elements))) +} +/** + * A function to calculate the inverse document frequency for + * a posting. This is shared between the builder and the index + * + * @private + * @param {object} posting - The posting for a given term + * @param {number} documentCount - The total number of documents. + */ +lunr.idf = function (posting, documentCount) { + var documentsWithTerm = 0 + + for (var fieldName in posting) { + if (fieldName == '_index') continue // Ignore the term index, its not a field + documentsWithTerm += Object.keys(posting[fieldName]).length + } + + var x = (documentCount - documentsWithTerm + 0.5) / (documentsWithTerm + 0.5) + + return Math.log(1 + Math.abs(x)) +} + +/** + * A token wraps a string representation of a token + * as it is passed through the text processing pipeline. + * + * @constructor + * @param {string} [str=''] - The string token being wrapped. + * @param {object} [metadata={}] - Metadata associated with this token. + */ +lunr.Token = function (str, metadata) { + this.str = str || "" + this.metadata = metadata || {} +} + +/** + * Returns the token string that is being wrapped by this object. + * + * @returns {string} + */ +lunr.Token.prototype.toString = function () { + return this.str +} + +/** + * A token update function is used when updating or optionally + * when cloning a token. + * + * @callback lunr.Token~updateFunction + * @param {string} str - The string representation of the token. + * @param {Object} metadata - All metadata associated with this token. + */ + +/** + * Applies the given function to the wrapped string token. + * + * @example + * token.update(function (str, metadata) { + * return str.toUpperCase() + * }) + * + * @param {lunr.Token~updateFunction} fn - A function to apply to the token string. + * @returns {lunr.Token} + */ +lunr.Token.prototype.update = function (fn) { + this.str = fn(this.str, this.metadata) + return this +} + +/** + * Creates a clone of this token. Optionally a function can be + * applied to the cloned token. + * + * @param {lunr.Token~updateFunction} [fn] - An optional function to apply to the cloned token. + * @returns {lunr.Token} + */ +lunr.Token.prototype.clone = function (fn) { + fn = fn || function (s) { return s } + return new lunr.Token (fn(this.str, this.metadata), this.metadata) +} +/*! + * lunr.tokenizer + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A function for splitting a string into tokens ready to be inserted into + * the search index. Uses `lunr.tokenizer.separator` to split strings, change + * the value of this property to change how strings are split into tokens. + * + * This tokenizer will convert its parameter to a string by calling `toString` and + * then will split this string on the character in `lunr.tokenizer.separator`. + * Arrays will have their elements converted to strings and wrapped in a lunr.Token. + * + * Optional metadata can be passed to the tokenizer, this metadata will be cloned and + * added as metadata to every token that is created from the object to be tokenized. + * + * @static + * @param {?(string|object|object[])} obj - The object to convert into tokens + * @param {?object} metadata - Optional metadata to associate with every token + * @returns {lunr.Token[]} + * @see {@link lunr.Pipeline} + */ +lunr.tokenizer = function (obj, metadata) { + if (obj == null || obj == undefined) { + return [] + } + + if (Array.isArray(obj)) { + return obj.map(function (t) { + return new lunr.Token( + lunr.utils.asString(t).toLowerCase(), + lunr.utils.clone(metadata) + ) + }) + } + + var str = obj.toString().toLowerCase(), + len = str.length, + tokens = [] + + for (var sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) { + var char = str.charAt(sliceEnd), + sliceLength = sliceEnd - sliceStart + + if ((char.match(lunr.tokenizer.separator) || sliceEnd == len)) { + + if (sliceLength > 0) { + var tokenMetadata = lunr.utils.clone(metadata) || {} + tokenMetadata["position"] = [sliceStart, sliceLength] + tokenMetadata["index"] = tokens.length + + tokens.push( + new lunr.Token ( + str.slice(sliceStart, sliceEnd), + tokenMetadata + ) + ) + } + + sliceStart = sliceEnd + 1 + } + + } + + return tokens +} + +/** + * The separator used to split a string into tokens. Override this property to change the behaviour of + * `lunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens. + * + * @static + * @see lunr.tokenizer + */ +lunr.tokenizer.separator = /[\s\-]+/ +/*! + * lunr.Pipeline + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.Pipelines maintain an ordered list of functions to be applied to all + * tokens in documents entering the search index and queries being ran against + * the index. + * + * An instance of lunr.Index created with the lunr shortcut will contain a + * pipeline with a stop word filter and an English language stemmer. Extra + * functions can be added before or after either of these functions or these + * default functions can be removed. + * + * When run the pipeline will call each function in turn, passing a token, the + * index of that token in the original list of all tokens and finally a list of + * all the original tokens. + * + * The output of functions in the pipeline will be passed to the next function + * in the pipeline. To exclude a token from entering the index the function + * should return undefined, the rest of the pipeline will not be called with + * this token. + * + * For serialisation of pipelines to work, all functions used in an instance of + * a pipeline should be registered with lunr.Pipeline. Registered functions can + * then be loaded. If trying to load a serialised pipeline that uses functions + * that are not registered an error will be thrown. + * + * If not planning on serialising the pipeline then registering pipeline functions + * is not necessary. + * + * @constructor + */ +lunr.Pipeline = function () { + this._stack = [] +} + +lunr.Pipeline.registeredFunctions = Object.create(null) + +/** + * A pipeline function maps lunr.Token to lunr.Token. A lunr.Token contains the token + * string as well as all known metadata. A pipeline function can mutate the token string + * or mutate (or add) metadata for a given token. + * + * A pipeline function can indicate that the passed token should be discarded by returning + * null, undefined or an empty string. This token will not be passed to any downstream pipeline + * functions and will not be added to the index. + * + * Multiple tokens can be returned by returning an array of tokens. Each token will be passed + * to any downstream pipeline functions and all will returned tokens will be added to the index. + * + * Any number of pipeline functions may be chained together using a lunr.Pipeline. + * + * @interface lunr.PipelineFunction + * @param {lunr.Token} token - A token from the document being processed. + * @param {number} i - The index of this token in the complete list of tokens for this document/field. + * @param {lunr.Token[]} tokens - All tokens for this document/field. + * @returns {(?lunr.Token|lunr.Token[])} + */ + +/** + * Register a function with the pipeline. + * + * Functions that are used in the pipeline should be registered if the pipeline + * needs to be serialised, or a serialised pipeline needs to be loaded. + * + * Registering a function does not add it to a pipeline, functions must still be + * added to instances of the pipeline for them to be used when running a pipeline. + * + * @param {lunr.PipelineFunction} fn - The function to check for. + * @param {String} label - The label to register this function with + */ +lunr.Pipeline.registerFunction = function (fn, label) { + if (label in this.registeredFunctions) { + lunr.utils.warn('Overwriting existing registered function: ' + label) + } + + fn.label = label + lunr.Pipeline.registeredFunctions[fn.label] = fn +} + +/** + * Warns if the function is not registered as a Pipeline function. + * + * @param {lunr.PipelineFunction} fn - The function to check for. + * @private + */ +lunr.Pipeline.warnIfFunctionNotRegistered = function (fn) { + var isRegistered = fn.label && (fn.label in this.registeredFunctions) + + if (!isRegistered) { + lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\n', fn) + } +} + +/** + * Loads a previously serialised pipeline. + * + * All functions to be loaded must already be registered with lunr.Pipeline. + * If any function from the serialised data has not been registered then an + * error will be thrown. + * + * @param {Object} serialised - The serialised pipeline to load. + * @returns {lunr.Pipeline} + */ +lunr.Pipeline.load = function (serialised) { + var pipeline = new lunr.Pipeline + + serialised.forEach(function (fnName) { + var fn = lunr.Pipeline.registeredFunctions[fnName] + + if (fn) { + pipeline.add(fn) + } else { + throw new Error('Cannot load unregistered function: ' + fnName) + } + }) + + return pipeline +} + +/** + * Adds new functions to the end of the pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction[]} functions - Any number of functions to add to the pipeline. + */ +lunr.Pipeline.prototype.add = function () { + var fns = Array.prototype.slice.call(arguments) + + fns.forEach(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + this._stack.push(fn) + }, this) +} + +/** + * Adds a single function after a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. + * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. + */ +lunr.Pipeline.prototype.after = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + if (pos == -1) { + throw new Error('Cannot find existingFn') + } + + pos = pos + 1 + this._stack.splice(pos, 0, newFn) +} + +/** + * Adds a single function before a function that already exists in the + * pipeline. + * + * Logs a warning if the function has not been registered. + * + * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline. + * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline. + */ +lunr.Pipeline.prototype.before = function (existingFn, newFn) { + lunr.Pipeline.warnIfFunctionNotRegistered(newFn) + + var pos = this._stack.indexOf(existingFn) + if (pos == -1) { + throw new Error('Cannot find existingFn') + } + + this._stack.splice(pos, 0, newFn) +} + +/** + * Removes a function from the pipeline. + * + * @param {lunr.PipelineFunction} fn The function to remove from the pipeline. + */ +lunr.Pipeline.prototype.remove = function (fn) { + var pos = this._stack.indexOf(fn) + if (pos == -1) { + return + } + + this._stack.splice(pos, 1) +} + +/** + * Runs the current list of functions that make up the pipeline against the + * passed tokens. + * + * @param {Array} tokens The tokens to run through the pipeline. + * @returns {Array} + */ +lunr.Pipeline.prototype.run = function (tokens) { + var stackLength = this._stack.length + + for (var i = 0; i < stackLength; i++) { + var fn = this._stack[i] + var memo = [] + + for (var j = 0; j < tokens.length; j++) { + var result = fn(tokens[j], j, tokens) + + if (result === null || result === void 0 || result === '') continue + + if (Array.isArray(result)) { + for (var k = 0; k < result.length; k++) { + memo.push(result[k]) + } + } else { + memo.push(result) + } + } + + tokens = memo + } + + return tokens +} + +/** + * Convenience method for passing a string through a pipeline and getting + * strings out. This method takes care of wrapping the passed string in a + * token and mapping the resulting tokens back to strings. + * + * @param {string} str - The string to pass through the pipeline. + * @param {?object} metadata - Optional metadata to associate with the token + * passed to the pipeline. + * @returns {string[]} + */ +lunr.Pipeline.prototype.runString = function (str, metadata) { + var token = new lunr.Token (str, metadata) + + return this.run([token]).map(function (t) { + return t.toString() + }) +} + +/** + * Resets the pipeline by removing any existing processors. + * + */ +lunr.Pipeline.prototype.reset = function () { + this._stack = [] +} + +/** + * Returns a representation of the pipeline ready for serialisation. + * + * Logs a warning if the function has not been registered. + * + * @returns {Array} + */ +lunr.Pipeline.prototype.toJSON = function () { + return this._stack.map(function (fn) { + lunr.Pipeline.warnIfFunctionNotRegistered(fn) + + return fn.label + }) +} +/*! + * lunr.Vector + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A vector is used to construct the vector space of documents and queries. These + * vectors support operations to determine the similarity between two documents or + * a document and a query. + * + * Normally no parameters are required for initializing a vector, but in the case of + * loading a previously dumped vector the raw elements can be provided to the constructor. + * + * For performance reasons vectors are implemented with a flat array, where an elements + * index is immediately followed by its value. E.g. [index, value, index, value]. This + * allows the underlying array to be as sparse as possible and still offer decent + * performance when being used for vector calculations. + * + * @constructor + * @param {Number[]} [elements] - The flat list of element index and element value pairs. + */ +lunr.Vector = function (elements) { + this._magnitude = 0 + this.elements = elements || [] +} + + +/** + * Calculates the position within the vector to insert a given index. + * + * This is used internally by insert and upsert. If there are duplicate indexes then + * the position is returned as if the value for that index were to be updated, but it + * is the callers responsibility to check whether there is a duplicate at that index + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @returns {Number} + */ +lunr.Vector.prototype.positionForIndex = function (index) { + // For an empty vector the tuple can be inserted at the beginning + if (this.elements.length == 0) { + return 0 + } + + var start = 0, + end = this.elements.length / 2, + sliceLength = end - start, + pivotPoint = Math.floor(sliceLength / 2), + pivotIndex = this.elements[pivotPoint * 2] + + while (sliceLength > 1) { + if (pivotIndex < index) { + start = pivotPoint + } + + if (pivotIndex > index) { + end = pivotPoint + } + + if (pivotIndex == index) { + break + } + + sliceLength = end - start + pivotPoint = start + Math.floor(sliceLength / 2) + pivotIndex = this.elements[pivotPoint * 2] + } + + if (pivotIndex == index) { + return pivotPoint * 2 + } + + if (pivotIndex > index) { + return pivotPoint * 2 + } + + if (pivotIndex < index) { + return (pivotPoint + 1) * 2 + } +} + +/** + * Inserts an element at an index within the vector. + * + * Does not allow duplicates, will throw an error if there is already an entry + * for this index. + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @param {Number} val - The value to be inserted into the vector. + */ +lunr.Vector.prototype.insert = function (insertIdx, val) { + this.upsert(insertIdx, val, function () { + throw "duplicate index" + }) +} + +/** + * Inserts or updates an existing index within the vector. + * + * @param {Number} insertIdx - The index at which the element should be inserted. + * @param {Number} val - The value to be inserted into the vector. + * @param {function} fn - A function that is called for updates, the existing value and the + * requested value are passed as arguments + */ +lunr.Vector.prototype.upsert = function (insertIdx, val, fn) { + this._magnitude = 0 + var position = this.positionForIndex(insertIdx) + + if (this.elements[position] == insertIdx) { + this.elements[position + 1] = fn(this.elements[position + 1], val) + } else { + this.elements.splice(position, 0, insertIdx, val) + } +} + +/** + * Calculates the magnitude of this vector. + * + * @returns {Number} + */ +lunr.Vector.prototype.magnitude = function () { + if (this._magnitude) return this._magnitude + + var sumOfSquares = 0, + elementsLength = this.elements.length + + for (var i = 1; i < elementsLength; i += 2) { + var val = this.elements[i] + sumOfSquares += val * val + } + + return this._magnitude = Math.sqrt(sumOfSquares) +} + +/** + * Calculates the dot product of this vector and another vector. + * + * @param {lunr.Vector} otherVector - The vector to compute the dot product with. + * @returns {Number} + */ +lunr.Vector.prototype.dot = function (otherVector) { + var dotProduct = 0, + a = this.elements, b = otherVector.elements, + aLen = a.length, bLen = b.length, + aVal = 0, bVal = 0, + i = 0, j = 0 + + while (i < aLen && j < bLen) { + aVal = a[i], bVal = b[j] + if (aVal < bVal) { + i += 2 + } else if (aVal > bVal) { + j += 2 + } else if (aVal == bVal) { + dotProduct += a[i + 1] * b[j + 1] + i += 2 + j += 2 + } + } + + return dotProduct +} + +/** + * Calculates the similarity between this vector and another vector. + * + * @param {lunr.Vector} otherVector - The other vector to calculate the + * similarity with. + * @returns {Number} + */ +lunr.Vector.prototype.similarity = function (otherVector) { + return this.dot(otherVector) / this.magnitude() || 0 +} + +/** + * Converts the vector to an array of the elements within the vector. + * + * @returns {Number[]} + */ +lunr.Vector.prototype.toArray = function () { + var output = new Array (this.elements.length / 2) + + for (var i = 1, j = 0; i < this.elements.length; i += 2, j++) { + output[j] = this.elements[i] + } + + return output +} + +/** + * A JSON serializable representation of the vector. + * + * @returns {Number[]} + */ +lunr.Vector.prototype.toJSON = function () { + return this.elements +} +/* eslint-disable */ +/*! + * lunr.stemmer + * Copyright (C) 2020 Oliver Nightingale + * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt + */ + +/** + * lunr.stemmer is an english language stemmer, this is a JavaScript + * implementation of the PorterStemmer taken from http://tartarus.org/~martin + * + * @static + * @implements {lunr.PipelineFunction} + * @param {lunr.Token} token - The string to stem + * @returns {lunr.Token} + * @see {@link lunr.Pipeline} + * @function + */ +lunr.stemmer = (function(){ + var step2list = { + "ational" : "ate", + "tional" : "tion", + "enci" : "ence", + "anci" : "ance", + "izer" : "ize", + "bli" : "ble", + "alli" : "al", + "entli" : "ent", + "eli" : "e", + "ousli" : "ous", + "ization" : "ize", + "ation" : "ate", + "ator" : "ate", + "alism" : "al", + "iveness" : "ive", + "fulness" : "ful", + "ousness" : "ous", + "aliti" : "al", + "iviti" : "ive", + "biliti" : "ble", + "logi" : "log" + }, + + step3list = { + "icate" : "ic", + "ative" : "", + "alize" : "al", + "iciti" : "ic", + "ical" : "ic", + "ful" : "", + "ness" : "" + }, + + c = "[^aeiou]", // consonant + v = "[aeiouy]", // vowel + C = c + "[^aeiouy]*", // consonant sequence + V = v + "[aeiou]*", // vowel sequence + + mgr0 = "^(" + C + ")?" + V + C, // [C]VC... is m>0 + meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$", // [C]VC[V] is m=1 + mgr1 = "^(" + C + ")?" + V + C + V + C, // [C]VCVC... is m>1 + s_v = "^(" + C + ")?" + v; // vowel in stem + + var re_mgr0 = new RegExp(mgr0); + var re_mgr1 = new RegExp(mgr1); + var re_meq1 = new RegExp(meq1); + var re_s_v = new RegExp(s_v); + + var re_1a = /^(.+?)(ss|i)es$/; + var re2_1a = /^(.+?)([^s])s$/; + var re_1b = /^(.+?)eed$/; + var re2_1b = /^(.+?)(ed|ing)$/; + var re_1b_2 = /.$/; + var re2_1b_2 = /(at|bl|iz)$/; + var re3_1b_2 = new RegExp("([^aeiouylsz])\\1$"); + var re4_1b_2 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var re_1c = /^(.+?[^aeiou])y$/; + var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + + var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + + var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + var re2_4 = /^(.+?)(s|t)(ion)$/; + + var re_5 = /^(.+?)e$/; + var re_5_1 = /ll$/; + var re3_5 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + + var porterStemmer = function porterStemmer(w) { + var stem, + suffix, + firstch, + re, + re2, + re3, + re4; + + if (w.length < 3) { return w; } + + firstch = w.substr(0,1); + if (firstch == "y") { + w = firstch.toUpperCase() + w.substr(1); + } + + // Step 1a + re = re_1a + re2 = re2_1a; + + if (re.test(w)) { w = w.replace(re,"$1$2"); } + else if (re2.test(w)) { w = w.replace(re2,"$1$2"); } + + // Step 1b + re = re_1b; + re2 = re2_1b; + if (re.test(w)) { + var fp = re.exec(w); + re = re_mgr0; + if (re.test(fp[1])) { + re = re_1b_2; + w = w.replace(re,""); + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = re_s_v; + if (re2.test(stem)) { + w = stem; + re2 = re2_1b_2; + re3 = re3_1b_2; + re4 = re4_1b_2; + if (re2.test(w)) { w = w + "e"; } + else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,""); } + else if (re4.test(w)) { w = w + "e"; } + } + } + + // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say) + re = re_1c; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + w = stem + "i"; + } + + // Step 2 + re = re_2; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step2list[suffix]; + } + } + + // Step 3 + re = re_3; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = re_mgr0; + if (re.test(stem)) { + w = stem + step3list[suffix]; + } + } + + // Step 4 + re = re_4; + re2 = re2_4; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + if (re.test(stem)) { + w = stem; + } + } else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = re_mgr1; + if (re2.test(stem)) { + w = stem; + } + } + + // Step 5 + re = re_5; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = re_mgr1; + re2 = re_meq1; + re3 = re3_5; + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) { + w = stem; + } + } + + re = re_5_1; + re2 = re_mgr1; + if (re.test(w) && re2.test(w)) { + re = re_1b_2; + w = w.replace(re,""); + } + + // and turn initial Y back to y + + if (firstch == "y") { + w = firstch.toLowerCase() + w.substr(1); + } + + return w; + }; + + return function (token) { + return token.update(porterStemmer); + } +})(); + +lunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer') +/*! + * lunr.stopWordFilter + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.generateStopWordFilter builds a stopWordFilter function from the provided + * list of stop words. + * + * The built in lunr.stopWordFilter is built using this generator and can be used + * to generate custom stopWordFilters for applications or non English languages. + * + * @function + * @param {Array} token The token to pass through the filter + * @returns {lunr.PipelineFunction} + * @see lunr.Pipeline + * @see lunr.stopWordFilter + */ +lunr.generateStopWordFilter = function (stopWords) { + var words = stopWords.reduce(function (memo, stopWord) { + memo[stopWord] = stopWord + return memo + }, {}) + + return function (token) { + if (token && words[token.toString()] !== token.toString()) return token + } +} + +/** + * lunr.stopWordFilter is an English language stop word list filter, any words + * contained in the list will not be passed through the filter. + * + * This is intended to be used in the Pipeline. If the token does not pass the + * filter then undefined will be returned. + * + * @function + * @implements {lunr.PipelineFunction} + * @params {lunr.Token} token - A token to check for being a stop word. + * @returns {lunr.Token} + * @see {@link lunr.Pipeline} + */ +lunr.stopWordFilter = lunr.generateStopWordFilter([ + 'a', + 'able', + 'about', + 'across', + 'after', + 'all', + 'almost', + 'also', + 'am', + 'among', + 'an', + 'and', + 'any', + 'are', + 'as', + 'at', + 'be', + 'because', + 'been', + 'but', + 'by', + 'can', + 'cannot', + 'could', + 'dear', + 'did', + 'do', + 'does', + 'either', + 'else', + 'ever', + 'every', + 'for', + 'from', + 'get', + 'got', + 'had', + 'has', + 'have', + 'he', + 'her', + 'hers', + 'him', + 'his', + 'how', + 'however', + 'i', + 'if', + 'in', + 'into', + 'is', + 'it', + 'its', + 'just', + 'least', + 'let', + 'like', + 'likely', + 'may', + 'me', + 'might', + 'most', + 'must', + 'my', + 'neither', + 'no', + 'nor', + 'not', + 'of', + 'off', + 'often', + 'on', + 'only', + 'or', + 'other', + 'our', + 'own', + 'rather', + 'said', + 'say', + 'says', + 'she', + 'should', + 'since', + 'so', + 'some', + 'than', + 'that', + 'the', + 'their', + 'them', + 'then', + 'there', + 'these', + 'they', + 'this', + 'tis', + 'to', + 'too', + 'twas', + 'us', + 'wants', + 'was', + 'we', + 'were', + 'what', + 'when', + 'where', + 'which', + 'while', + 'who', + 'whom', + 'why', + 'will', + 'with', + 'would', + 'yet', + 'you', + 'your' +]) + +lunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter') +/*! + * lunr.trimmer + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.trimmer is a pipeline function for trimming non word + * characters from the beginning and end of tokens before they + * enter the index. + * + * This implementation may not work correctly for non latin + * characters and should either be removed or adapted for use + * with languages with non-latin characters. + * + * @static + * @implements {lunr.PipelineFunction} + * @param {lunr.Token} token The token to pass through the filter + * @returns {lunr.Token} + * @see lunr.Pipeline + */ +lunr.trimmer = function (token) { + return token.update(function (s) { + return s.replace(/^\W+/, '').replace(/\W+$/, '') + }) +} + +lunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer') +/*! + * lunr.TokenSet + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * A token set is used to store the unique list of all tokens + * within an index. Token sets are also used to represent an + * incoming query to the index, this query token set and index + * token set are then intersected to find which tokens to look + * up in the inverted index. + * + * A token set can hold multiple tokens, as in the case of the + * index token set, or it can hold a single token as in the + * case of a simple query token set. + * + * Additionally token sets are used to perform wildcard matching. + * Leading, contained and trailing wildcards are supported, and + * from this edit distance matching can also be provided. + * + * Token sets are implemented as a minimal finite state automata, + * where both common prefixes and suffixes are shared between tokens. + * This helps to reduce the space used for storing the token set. + * + * @constructor + */ +lunr.TokenSet = function () { + this.final = false + this.edges = {} + this.id = lunr.TokenSet._nextId + lunr.TokenSet._nextId += 1 +} + +/** + * Keeps track of the next, auto increment, identifier to assign + * to a new tokenSet. + * + * TokenSets require a unique identifier to be correctly minimised. + * + * @private + */ +lunr.TokenSet._nextId = 1 + +/** + * Creates a TokenSet instance from the given sorted array of words. + * + * @param {String[]} arr - A sorted array of strings to create the set from. + * @returns {lunr.TokenSet} + * @throws Will throw an error if the input array is not sorted. + */ +lunr.TokenSet.fromArray = function (arr) { + var builder = new lunr.TokenSet.Builder + + for (var i = 0, len = arr.length; i < len; i++) { + builder.insert(arr[i]) + } + + builder.finish() + return builder.root +} + +/** + * Creates a token set from a query clause. + * + * @private + * @param {Object} clause - A single clause from lunr.Query. + * @param {string} clause.term - The query clause term. + * @param {number} [clause.editDistance] - The optional edit distance for the term. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.fromClause = function (clause) { + if ('editDistance' in clause) { + return lunr.TokenSet.fromFuzzyString(clause.term, clause.editDistance) + } else { + return lunr.TokenSet.fromString(clause.term) + } +} + +/** + * Creates a token set representing a single string with a specified + * edit distance. + * + * Insertions, deletions, substitutions and transpositions are each + * treated as an edit distance of 1. + * + * Increasing the allowed edit distance will have a dramatic impact + * on the performance of both creating and intersecting these TokenSets. + * It is advised to keep the edit distance less than 3. + * + * @param {string} str - The string to create the token set from. + * @param {number} editDistance - The allowed edit distance to match. + * @returns {lunr.Vector} + */ +lunr.TokenSet.fromFuzzyString = function (str, editDistance) { + var root = new lunr.TokenSet + + var stack = [{ + node: root, + editsRemaining: editDistance, + str: str + }] + + while (stack.length) { + var frame = stack.pop() + + // no edit + if (frame.str.length > 0) { + var char = frame.str.charAt(0), + noEditNode + + if (char in frame.node.edges) { + noEditNode = frame.node.edges[char] + } else { + noEditNode = new lunr.TokenSet + frame.node.edges[char] = noEditNode + } + + if (frame.str.length == 1) { + noEditNode.final = true + } + + stack.push({ + node: noEditNode, + editsRemaining: frame.editsRemaining, + str: frame.str.slice(1) + }) + } + + if (frame.editsRemaining == 0) { + continue + } + + // insertion + if ("*" in frame.node.edges) { + var insertionNode = frame.node.edges["*"] + } else { + var insertionNode = new lunr.TokenSet + frame.node.edges["*"] = insertionNode + } + + if (frame.str.length == 0) { + insertionNode.final = true + } + + stack.push({ + node: insertionNode, + editsRemaining: frame.editsRemaining - 1, + str: frame.str + }) + + // deletion + // can only do a deletion if we have enough edits remaining + // and if there are characters left to delete in the string + if (frame.str.length > 1) { + stack.push({ + node: frame.node, + editsRemaining: frame.editsRemaining - 1, + str: frame.str.slice(1) + }) + } + + // deletion + // just removing the last character from the str + if (frame.str.length == 1) { + frame.node.final = true + } + + // substitution + // can only do a substitution if we have enough edits remaining + // and if there are characters left to substitute + if (frame.str.length >= 1) { + if ("*" in frame.node.edges) { + var substitutionNode = frame.node.edges["*"] + } else { + var substitutionNode = new lunr.TokenSet + frame.node.edges["*"] = substitutionNode + } + + if (frame.str.length == 1) { + substitutionNode.final = true + } + + stack.push({ + node: substitutionNode, + editsRemaining: frame.editsRemaining - 1, + str: frame.str.slice(1) + }) + } + + // transposition + // can only do a transposition if there are edits remaining + // and there are enough characters to transpose + if (frame.str.length > 1) { + var charA = frame.str.charAt(0), + charB = frame.str.charAt(1), + transposeNode + + if (charB in frame.node.edges) { + transposeNode = frame.node.edges[charB] + } else { + transposeNode = new lunr.TokenSet + frame.node.edges[charB] = transposeNode + } + + if (frame.str.length == 1) { + transposeNode.final = true + } + + stack.push({ + node: transposeNode, + editsRemaining: frame.editsRemaining - 1, + str: charA + frame.str.slice(2) + }) + } + } + + return root +} + +/** + * Creates a TokenSet from a string. + * + * The string may contain one or more wildcard characters (*) + * that will allow wildcard matching when intersecting with + * another TokenSet. + * + * @param {string} str - The string to create a TokenSet from. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.fromString = function (str) { + var node = new lunr.TokenSet, + root = node + + /* + * Iterates through all characters within the passed string + * appending a node for each character. + * + * When a wildcard character is found then a self + * referencing edge is introduced to continually match + * any number of any characters. + */ + for (var i = 0, len = str.length; i < len; i++) { + var char = str[i], + final = (i == len - 1) + + if (char == "*") { + node.edges[char] = node + node.final = final + + } else { + var next = new lunr.TokenSet + next.final = final + + node.edges[char] = next + node = next + } + } + + return root +} + +/** + * Converts this TokenSet into an array of strings + * contained within the TokenSet. + * + * This is not intended to be used on a TokenSet that + * contains wildcards, in these cases the results are + * undefined and are likely to cause an infinite loop. + * + * @returns {string[]} + */ +lunr.TokenSet.prototype.toArray = function () { + var words = [] + + var stack = [{ + prefix: "", + node: this + }] + + while (stack.length) { + var frame = stack.pop(), + edges = Object.keys(frame.node.edges), + len = edges.length + + if (frame.node.final) { + /* In Safari, at this point the prefix is sometimes corrupted, see: + * https://github.com/olivernn/lunr.js/issues/279 Calling any + * String.prototype method forces Safari to "cast" this string to what + * it's supposed to be, fixing the bug. */ + frame.prefix.charAt(0) + words.push(frame.prefix) + } + + for (var i = 0; i < len; i++) { + var edge = edges[i] + + stack.push({ + prefix: frame.prefix.concat(edge), + node: frame.node.edges[edge] + }) + } + } + + return words +} + +/** + * Generates a string representation of a TokenSet. + * + * This is intended to allow TokenSets to be used as keys + * in objects, largely to aid the construction and minimisation + * of a TokenSet. As such it is not designed to be a human + * friendly representation of the TokenSet. + * + * @returns {string} + */ +lunr.TokenSet.prototype.toString = function () { + // NOTE: Using Object.keys here as this.edges is very likely + // to enter 'hash-mode' with many keys being added + // + // avoiding a for-in loop here as it leads to the function + // being de-optimised (at least in V8). From some simple + // benchmarks the performance is comparable, but allowing + // V8 to optimize may mean easy performance wins in the future. + + if (this._str) { + return this._str + } + + var str = this.final ? '1' : '0', + labels = Object.keys(this.edges).sort(), + len = labels.length + + for (var i = 0; i < len; i++) { + var label = labels[i], + node = this.edges[label] + + str = str + label + node.id + } + + return str +} + +/** + * Returns a new TokenSet that is the intersection of + * this TokenSet and the passed TokenSet. + * + * This intersection will take into account any wildcards + * contained within the TokenSet. + * + * @param {lunr.TokenSet} b - An other TokenSet to intersect with. + * @returns {lunr.TokenSet} + */ +lunr.TokenSet.prototype.intersect = function (b) { + var output = new lunr.TokenSet, + frame = undefined + + var stack = [{ + qNode: b, + output: output, + node: this + }] + + while (stack.length) { + frame = stack.pop() + + // NOTE: As with the #toString method, we are using + // Object.keys and a for loop instead of a for-in loop + // as both of these objects enter 'hash' mode, causing + // the function to be de-optimised in V8 + var qEdges = Object.keys(frame.qNode.edges), + qLen = qEdges.length, + nEdges = Object.keys(frame.node.edges), + nLen = nEdges.length + + for (var q = 0; q < qLen; q++) { + var qEdge = qEdges[q] + + for (var n = 0; n < nLen; n++) { + var nEdge = nEdges[n] + + if (nEdge == qEdge || qEdge == '*') { + var node = frame.node.edges[nEdge], + qNode = frame.qNode.edges[qEdge], + final = node.final && qNode.final, + next = undefined + + if (nEdge in frame.output.edges) { + // an edge already exists for this character + // no need to create a new node, just set the finality + // bit unless this node is already final + next = frame.output.edges[nEdge] + next.final = next.final || final + + } else { + // no edge exists yet, must create one + // set the finality bit and insert it + // into the output + next = new lunr.TokenSet + next.final = final + frame.output.edges[nEdge] = next + } + + stack.push({ + qNode: qNode, + output: next, + node: node + }) + } + } + } + } + + return output +} +lunr.TokenSet.Builder = function () { + this.previousWord = "" + this.root = new lunr.TokenSet + this.uncheckedNodes = [] + this.minimizedNodes = {} +} + +lunr.TokenSet.Builder.prototype.insert = function (word) { + var node, + commonPrefix = 0 + + if (word < this.previousWord) { + throw new Error ("Out of order word insertion") + } + + for (var i = 0; i < word.length && i < this.previousWord.length; i++) { + if (word[i] != this.previousWord[i]) break + commonPrefix++ + } + + this.minimize(commonPrefix) + + if (this.uncheckedNodes.length == 0) { + node = this.root + } else { + node = this.uncheckedNodes[this.uncheckedNodes.length - 1].child + } + + for (var i = commonPrefix; i < word.length; i++) { + var nextNode = new lunr.TokenSet, + char = word[i] + + node.edges[char] = nextNode + + this.uncheckedNodes.push({ + parent: node, + char: char, + child: nextNode + }) + + node = nextNode + } + + node.final = true + this.previousWord = word +} + +lunr.TokenSet.Builder.prototype.finish = function () { + this.minimize(0) +} + +lunr.TokenSet.Builder.prototype.minimize = function (downTo) { + for (var i = this.uncheckedNodes.length - 1; i >= downTo; i--) { + var node = this.uncheckedNodes[i], + childKey = node.child.toString() + + if (childKey in this.minimizedNodes) { + node.parent.edges[node.char] = this.minimizedNodes[childKey] + } else { + // Cache the key for this node since + // we know it can't change anymore + node.child._str = childKey + + this.minimizedNodes[childKey] = node.child + } + + this.uncheckedNodes.pop() + } +} +/*! + * lunr.Index + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * An index contains the built index of all documents and provides a query interface + * to the index. + * + * Usually instances of lunr.Index will not be created using this constructor, instead + * lunr.Builder should be used to construct new indexes, or lunr.Index.load should be + * used to load previously built and serialized indexes. + * + * @constructor + * @param {Object} attrs - The attributes of the built search index. + * @param {Object} attrs.invertedIndex - An index of term/field to document reference. + * @param {Object} attrs.fieldVectors - Field vectors + * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens. + * @param {string[]} attrs.fields - The names of indexed document fields. + * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms. + */ +lunr.Index = function (attrs) { + this.invertedIndex = attrs.invertedIndex + this.fieldVectors = attrs.fieldVectors + this.tokenSet = attrs.tokenSet + this.fields = attrs.fields + this.pipeline = attrs.pipeline +} + +/** + * A result contains details of a document matching a search query. + * @typedef {Object} lunr.Index~Result + * @property {string} ref - The reference of the document this result represents. + * @property {number} score - A number between 0 and 1 representing how similar this document is to the query. + * @property {lunr.MatchData} matchData - Contains metadata about this match including which term(s) caused the match. + */ + +/** + * Although lunr provides the ability to create queries using lunr.Query, it also provides a simple + * query language which itself is parsed into an instance of lunr.Query. + * + * For programmatically building queries it is advised to directly use lunr.Query, the query language + * is best used for human entered text rather than program generated text. + * + * At its simplest queries can just be a single term, e.g. `hello`, multiple terms are also supported + * and will be combined with OR, e.g `hello world` will match documents that contain either 'hello' + * or 'world', though those that contain both will rank higher in the results. + * + * Wildcards can be included in terms to match one or more unspecified characters, these wildcards can + * be inserted anywhere within the term, and more than one wildcard can exist in a single term. Adding + * wildcards will increase the number of documents that will be found but can also have a negative + * impact on query performance, especially with wildcards at the beginning of a term. + * + * Terms can be restricted to specific fields, e.g. `title:hello`, only documents with the term + * hello in the title field will match this query. Using a field not present in the index will lead + * to an error being thrown. + * + * Modifiers can also be added to terms, lunr supports edit distance and boost modifiers on terms. A term + * boost will make documents matching that term score higher, e.g. `foo^5`. Edit distance is also supported + * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2. + * Avoid large values for edit distance to improve query performance. + * + * Each term also supports a presence modifier. By default a term's presence in document is optional, however + * this can be changed to either required or prohibited. For a term's presence to be required in a document the + * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and + * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not + * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'. + * + * To escape special characters the backslash character '\' can be used, this allows searches to include + * characters that would normally be considered modifiers, e.g. `foo\~2` will search for a term "foo~2" instead + * of attempting to apply a boost of 2 to the search term "foo". + * + * @typedef {string} lunr.Index~QueryString + * @example Simple single term query + * hello + * @example Multiple term query + * hello world + * @example term scoped to a field + * title:hello + * @example term with a boost of 10 + * hello^10 + * @example term with an edit distance of 2 + * hello~2 + * @example terms with presence modifiers + * -foo +bar baz + */ + +/** + * Performs a search against the index using lunr query syntax. + * + * Results will be returned sorted by their score, the most relevant results + * will be returned first. For details on how the score is calculated, please see + * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}. + * + * For more programmatic querying use lunr.Index#query. + * + * @param {lunr.Index~QueryString} queryString - A string containing a lunr query. + * @throws {lunr.QueryParseError} If the passed query string cannot be parsed. + * @returns {lunr.Index~Result[]} + */ +lunr.Index.prototype.search = function (queryString) { + return this.query(function (query) { + var parser = new lunr.QueryParser(queryString, query) + parser.parse() + }) +} + +/** + * A query builder callback provides a query object to be used to express + * the query to perform on the index. + * + * @callback lunr.Index~queryBuilder + * @param {lunr.Query} query - The query object to build up. + * @this lunr.Query + */ + +/** + * Performs a query against the index using the yielded lunr.Query object. + * + * If performing programmatic queries against the index, this method is preferred + * over lunr.Index#search so as to avoid the additional query parsing overhead. + * + * A query object is yielded to the supplied function which should be used to + * express the query to be run against the index. + * + * Note that although this function takes a callback parameter it is _not_ an + * asynchronous operation, the callback is just yielded a query object to be + * customized. + * + * @param {lunr.Index~queryBuilder} fn - A function that is used to build the query. + * @returns {lunr.Index~Result[]} + */ +lunr.Index.prototype.query = function (fn) { + // for each query clause + // * process terms + // * expand terms from token set + // * find matching documents and metadata + // * get document vectors + // * score documents + + var query = new lunr.Query(this.fields), + matchingFields = Object.create(null), + queryVectors = Object.create(null), + termFieldCache = Object.create(null), + requiredMatches = Object.create(null), + prohibitedMatches = Object.create(null) + + /* + * To support field level boosts a query vector is created per + * field. An empty vector is eagerly created to support negated + * queries. + */ + for (var i = 0; i < this.fields.length; i++) { + queryVectors[this.fields[i]] = new lunr.Vector + } + + fn.call(query, query) + + for (var i = 0; i < query.clauses.length; i++) { + /* + * Unless the pipeline has been disabled for this term, which is + * the case for terms with wildcards, we need to pass the clause + * term through the search pipeline. A pipeline returns an array + * of processed terms. Pipeline functions may expand the passed + * term, which means we may end up performing multiple index lookups + * for a single query term. + */ + var clause = query.clauses[i], + terms = null, + clauseMatches = lunr.Set.empty + + if (clause.usePipeline) { + terms = this.pipeline.runString(clause.term, { + fields: clause.fields + }) + } else { + terms = [clause.term] + } + + for (var m = 0; m < terms.length; m++) { + var term = terms[m] + + /* + * Each term returned from the pipeline needs to use the same query + * clause object, e.g. the same boost and or edit distance. The + * simplest way to do this is to re-use the clause object but mutate + * its term property. + */ + clause.term = term + + /* + * From the term in the clause we create a token set which will then + * be used to intersect the indexes token set to get a list of terms + * to lookup in the inverted index + */ + var termTokenSet = lunr.TokenSet.fromClause(clause), + expandedTerms = this.tokenSet.intersect(termTokenSet).toArray() + + /* + * If a term marked as required does not exist in the tokenSet it is + * impossible for the search to return any matches. We set all the field + * scoped required matches set to empty and stop examining any further + * clauses. + */ + if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) { + for (var k = 0; k < clause.fields.length; k++) { + var field = clause.fields[k] + requiredMatches[field] = lunr.Set.empty + } + + break + } + + for (var j = 0; j < expandedTerms.length; j++) { + /* + * For each term get the posting and termIndex, this is required for + * building the query vector. + */ + var expandedTerm = expandedTerms[j], + posting = this.invertedIndex[expandedTerm], + termIndex = posting._index + + for (var k = 0; k < clause.fields.length; k++) { + /* + * For each field that this query term is scoped by (by default + * all fields are in scope) we need to get all the document refs + * that have this term in that field. + * + * The posting is the entry in the invertedIndex for the matching + * term from above. + */ + var field = clause.fields[k], + fieldPosting = posting[field], + matchingDocumentRefs = Object.keys(fieldPosting), + termField = expandedTerm + "/" + field, + matchingDocumentsSet = new lunr.Set(matchingDocumentRefs) + + /* + * if the presence of this term is required ensure that the matching + * documents are added to the set of required matches for this clause. + * + */ + if (clause.presence == lunr.Query.presence.REQUIRED) { + clauseMatches = clauseMatches.union(matchingDocumentsSet) + + if (requiredMatches[field] === undefined) { + requiredMatches[field] = lunr.Set.complete + } + } + + /* + * if the presence of this term is prohibited ensure that the matching + * documents are added to the set of prohibited matches for this field, + * creating that set if it does not yet exist. + */ + if (clause.presence == lunr.Query.presence.PROHIBITED) { + if (prohibitedMatches[field] === undefined) { + prohibitedMatches[field] = lunr.Set.empty + } + + prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet) + + /* + * Prohibited matches should not be part of the query vector used for + * similarity scoring and no metadata should be extracted so we continue + * to the next field + */ + continue + } + + /* + * The query field vector is populated using the termIndex found for + * the term and a unit value with the appropriate boost applied. + * Using upsert because there could already be an entry in the vector + * for the term we are working with. In that case we just add the scores + * together. + */ + queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b }) + + /** + * If we've already seen this term, field combo then we've already collected + * the matching documents and metadata, no need to go through all that again + */ + if (termFieldCache[termField]) { + continue + } + + for (var l = 0; l < matchingDocumentRefs.length; l++) { + /* + * All metadata for this term/field/document triple + * are then extracted and collected into an instance + * of lunr.MatchData ready to be returned in the query + * results + */ + var matchingDocumentRef = matchingDocumentRefs[l], + matchingFieldRef = new lunr.FieldRef (matchingDocumentRef, field), + metadata = fieldPosting[matchingDocumentRef], + fieldMatch + + if ((fieldMatch = matchingFields[matchingFieldRef]) === undefined) { + matchingFields[matchingFieldRef] = new lunr.MatchData (expandedTerm, field, metadata) + } else { + fieldMatch.add(expandedTerm, field, metadata) + } + + } + + termFieldCache[termField] = true + } + } + } + + /** + * If the presence was required we need to update the requiredMatches field sets. + * We do this after all fields for the term have collected their matches because + * the clause terms presence is required in _any_ of the fields not _all_ of the + * fields. + */ + if (clause.presence === lunr.Query.presence.REQUIRED) { + for (var k = 0; k < clause.fields.length; k++) { + var field = clause.fields[k] + requiredMatches[field] = requiredMatches[field].intersect(clauseMatches) + } + } + } + + /** + * Need to combine the field scoped required and prohibited + * matching documents into a global set of required and prohibited + * matches + */ + var allRequiredMatches = lunr.Set.complete, + allProhibitedMatches = lunr.Set.empty + + for (var i = 0; i < this.fields.length; i++) { + var field = this.fields[i] + + if (requiredMatches[field]) { + allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field]) + } + + if (prohibitedMatches[field]) { + allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field]) + } + } + + var matchingFieldRefs = Object.keys(matchingFields), + results = [], + matches = Object.create(null) + + /* + * If the query is negated (contains only prohibited terms) + * we need to get _all_ fieldRefs currently existing in the + * index. This is only done when we know that the query is + * entirely prohibited terms to avoid any cost of getting all + * fieldRefs unnecessarily. + * + * Additionally, blank MatchData must be created to correctly + * populate the results. + */ + if (query.isNegated()) { + matchingFieldRefs = Object.keys(this.fieldVectors) + + for (var i = 0; i < matchingFieldRefs.length; i++) { + var matchingFieldRef = matchingFieldRefs[i] + var fieldRef = lunr.FieldRef.fromString(matchingFieldRef) + matchingFields[matchingFieldRef] = new lunr.MatchData + } + } + + for (var i = 0; i < matchingFieldRefs.length; i++) { + /* + * Currently we have document fields that match the query, but we + * need to return documents. The matchData and scores are combined + * from multiple fields belonging to the same document. + * + * Scores are calculated by field, using the query vectors created + * above, and combined into a final document score using addition. + */ + var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]), + docRef = fieldRef.docRef + + if (!allRequiredMatches.contains(docRef)) { + continue + } + + if (allProhibitedMatches.contains(docRef)) { + continue + } + + var fieldVector = this.fieldVectors[fieldRef], + score = queryVectors[fieldRef.fieldName].similarity(fieldVector), + docMatch + + if ((docMatch = matches[docRef]) !== undefined) { + docMatch.score += score + docMatch.matchData.combine(matchingFields[fieldRef]) + } else { + var match = { + ref: docRef, + score: score, + matchData: matchingFields[fieldRef] + } + matches[docRef] = match + results.push(match) + } + } + + /* + * Sort the results objects by score, highest first. + */ + return results.sort(function (a, b) { + return b.score - a.score + }) +} + +/** + * Prepares the index for JSON serialization. + * + * The schema for this JSON blob will be described in a + * separate JSON schema file. + * + * @returns {Object} + */ +lunr.Index.prototype.toJSON = function () { + var invertedIndex = Object.keys(this.invertedIndex) + .sort() + .map(function (term) { + return [term, this.invertedIndex[term]] + }, this) + + var fieldVectors = Object.keys(this.fieldVectors) + .map(function (ref) { + return [ref, this.fieldVectors[ref].toJSON()] + }, this) + + return { + version: lunr.version, + fields: this.fields, + fieldVectors: fieldVectors, + invertedIndex: invertedIndex, + pipeline: this.pipeline.toJSON() + } +} + +/** + * Loads a previously serialized lunr.Index + * + * @param {Object} serializedIndex - A previously serialized lunr.Index + * @returns {lunr.Index} + */ +lunr.Index.load = function (serializedIndex) { + var attrs = {}, + fieldVectors = {}, + serializedVectors = serializedIndex.fieldVectors, + invertedIndex = Object.create(null), + serializedInvertedIndex = serializedIndex.invertedIndex, + tokenSetBuilder = new lunr.TokenSet.Builder, + pipeline = lunr.Pipeline.load(serializedIndex.pipeline) + + if (serializedIndex.version != lunr.version) { + lunr.utils.warn("Version mismatch when loading serialised index. Current version of lunr '" + lunr.version + "' does not match serialized index '" + serializedIndex.version + "'") + } + + for (var i = 0; i < serializedVectors.length; i++) { + var tuple = serializedVectors[i], + ref = tuple[0], + elements = tuple[1] + + fieldVectors[ref] = new lunr.Vector(elements) + } + + for (var i = 0; i < serializedInvertedIndex.length; i++) { + var tuple = serializedInvertedIndex[i], + term = tuple[0], + posting = tuple[1] + + tokenSetBuilder.insert(term) + invertedIndex[term] = posting + } + + tokenSetBuilder.finish() + + attrs.fields = serializedIndex.fields + + attrs.fieldVectors = fieldVectors + attrs.invertedIndex = invertedIndex + attrs.tokenSet = tokenSetBuilder.root + attrs.pipeline = pipeline + + return new lunr.Index(attrs) +} +/*! + * lunr.Builder + * Copyright (C) 2020 Oliver Nightingale + */ + +/** + * lunr.Builder performs indexing on a set of documents and + * returns instances of lunr.Index ready for querying. + * + * All configuration of the index is done via the builder, the + * fields to index, the document reference, the text processing + * pipeline and document scoring parameters are all set on the + * builder before indexing. + * + * @constructor + * @property {string} _ref - Internal reference to the document reference field. + * @property {string[]} _fields - Internal reference to the document fields to index. + * @property {object} invertedIndex - The inverted index maps terms to document fields. + * @property {object} documentTermFrequencies - Keeps track of document term frequencies. + * @property {object} documentLengths - Keeps track of the length of documents added to the index. + * @property {lunr.tokenizer} tokenizer - Function for splitting strings into tokens for indexing. + * @property {lunr.Pipeline} pipeline - The pipeline performs text processing on tokens before indexing. + * @property {lunr.Pipeline} searchPipeline - A pipeline for processing search terms before querying the index. + * @property {number} documentCount - Keeps track of the total number of documents indexed. + * @property {number} _b - A parameter to control field length normalization, setting this to 0 disabled normalization, 1 fully normalizes field lengths, the default value is 0.75. + * @property {number} _k1 - A parameter to control how quickly an increase in term frequency results in term frequency saturation, the default value is 1.2. + * @property {number} termIndex - A counter incremented for each unique term, used to identify a terms position in the vector space. + * @property {array} metadataWhitelist - A list of metadata keys that have been whitelisted for entry in the index. + */ +lunr.Builder = function () { + this._ref = "id" + this._fields = Object.create(null) + this._documents = Object.create(null) + this.invertedIndex = Object.create(null) + this.fieldTermFrequencies = {} + this.fieldLengths = {} + this.tokenizer = lunr.tokenizer + this.pipeline = new lunr.Pipeline + this.searchPipeline = new lunr.Pipeline + this.documentCount = 0 + this._b = 0.75 + this._k1 = 1.2 + this.termIndex = 0 + this.metadataWhitelist = [] +} + +/** + * Sets the document field used as the document reference. Every document must have this field. + * The type of this field in the document should be a string, if it is not a string it will be + * coerced into a string by calling toString. + * + * The default ref is 'id'. + * + * The ref should _not_ be changed during indexing, it should be set before any documents are + * added to the index. Changing it during indexing can lead to inconsistent results. + * + * @param {string} ref - The name of the reference field in the document. + */ +lunr.Builder.prototype.ref = function (ref) { + this._ref = ref +} + +/** + * A function that is used to extract a field from a document. + * + * Lunr expects a field to be at the top level of a document, if however the field + * is deeply nested within a document an extractor function can be used to extract + * the right field for indexing. + * + * @callback fieldExtractor + * @param {object} doc - The document being added to the index. + * @returns {?(string|object|object[])} obj - The object that will be indexed for this field. + * @example Extracting a nested field + * function (doc) { return doc.nested.field } + */ + +/** + * Adds a field to the list of document fields that will be indexed. Every document being + * indexed should have this field. Null values for this field in indexed documents will + * not cause errors but will limit the chance of that document being retrieved by searches. + * + * All fields should be added before adding documents to the index. Adding fields after + * a document has been indexed will have no effect on already indexed documents. + * + * Fields can be boosted at build time. This allows terms within that field to have more + * importance when ranking search results. Use a field boost to specify that matches within + * one field are more important than other fields. + * + * @param {string} fieldName - The name of a field to index in all documents. + * @param {object} attributes - Optional attributes associated with this field. + * @param {number} [attributes.boost=1] - Boost applied to all terms within this field. + * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document. + * @throws {RangeError} fieldName cannot contain unsupported characters '/' + */ +lunr.Builder.prototype.field = function (fieldName, attributes) { + if (/\//.test(fieldName)) { + throw new RangeError ("Field '" + fieldName + "' contains illegal character '/'") + } + + this._fields[fieldName] = attributes || {} +} + +/** + * A parameter to tune the amount of field length normalisation that is applied when + * calculating relevance scores. A value of 0 will completely disable any normalisation + * and a value of 1 will fully normalise field lengths. The default is 0.75. Values of b + * will be clamped to the range 0 - 1. + * + * @param {number} number - The value to set for this tuning parameter. + */ +lunr.Builder.prototype.b = function (number) { + if (number < 0) { + this._b = 0 + } else if (number > 1) { + this._b = 1 + } else { + this._b = number + } +} + +/** + * A parameter that controls the speed at which a rise in term frequency results in term + * frequency saturation. The default value is 1.2. Setting this to a higher value will give + * slower saturation levels, a lower value will result in quicker saturation. + * + * @param {number} number - The value to set for this tuning parameter. + */ +lunr.Builder.prototype.k1 = function (number) { + this._k1 = number +} + +/** + * Adds a document to the index. + * + * Before adding fields to the index the index should have been fully setup, with the document + * ref and all fields to index already having been specified. + * + * The document must have a field name as specified by the ref (by default this is 'id') and + * it should have all fields defined for indexing, though null or undefined values will not + * cause errors. + * + * Entire documents can be boosted at build time. Applying a boost to a document indicates that + * this document should rank higher in search results than other documents. + * + * @param {object} doc - The document to add to the index. + * @param {object} attributes - Optional attributes associated with this document. + * @param {number} [attributes.boost=1] - Boost applied to all terms within this document. + */ +lunr.Builder.prototype.add = function (doc, attributes) { + var docRef = doc[this._ref], + fields = Object.keys(this._fields) + + this._documents[docRef] = attributes || {} + this.documentCount += 1 + + for (var i = 0; i < fields.length; i++) { + var fieldName = fields[i], + extractor = this._fields[fieldName].extractor, + field = extractor ? extractor(doc) : doc[fieldName], + tokens = this.tokenizer(field, { + fields: [fieldName] + }), + terms = this.pipeline.run(tokens), + fieldRef = new lunr.FieldRef (docRef, fieldName), + fieldTerms = Object.create(null) + + this.fieldTermFrequencies[fieldRef] = fieldTerms + this.fieldLengths[fieldRef] = 0 + + // store the length of this field for this document + this.fieldLengths[fieldRef] += terms.length + + // calculate term frequencies for this field + for (var j = 0; j < terms.length; j++) { + var term = terms[j] + + if (fieldTerms[term] == undefined) { + fieldTerms[term] = 0 + } + + fieldTerms[term] += 1 + + // add to inverted index + // create an initial posting if one doesn't exist + if (this.invertedIndex[term] == undefined) { + var posting = Object.create(null) + posting["_index"] = this.termIndex + this.termIndex += 1 + + for (var k = 0; k < fields.length; k++) { + posting[fields[k]] = Object.create(null) + } + + this.invertedIndex[term] = posting + } + + // add an entry for this term/fieldName/docRef to the invertedIndex + if (this.invertedIndex[term][fieldName][docRef] == undefined) { + this.invertedIndex[term][fieldName][docRef] = Object.create(null) + } + + // store all whitelisted metadata about this token in the + // inverted index + for (var l = 0; l < this.metadataWhitelist.length; l++) { + var metadataKey = this.metadataWhitelist[l], + metadata = term.metadata[metadataKey] + + if (this.invertedIndex[term][fieldName][docRef][metadataKey] == undefined) { + this.invertedIndex[term][fieldName][docRef][metadataKey] = [] + } + + this.invertedIndex[term][fieldName][docRef][metadataKey].push(metadata) + } + } + + } +} + +/** + * Calculates the average document length for this index + * + * @private + */ +lunr.Builder.prototype.calculateAverageFieldLengths = function () { + + var fieldRefs = Object.keys(this.fieldLengths), + numberOfFields = fieldRefs.length, + accumulator = {}, + documentsWithField = {} + + for (var i = 0; i < numberOfFields; i++) { + var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), + field = fieldRef.fieldName + + documentsWithField[field] || (documentsWithField[field] = 0) + documentsWithField[field] += 1 + + accumulator[field] || (accumulator[field] = 0) + accumulator[field] += this.fieldLengths[fieldRef] + } + + var fields = Object.keys(this._fields) + + for (var i = 0; i < fields.length; i++) { + var fieldName = fields[i] + accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName] + } + + this.averageFieldLength = accumulator +} + +/** + * Builds a vector space model of every document using lunr.Vector + * + * @private + */ +lunr.Builder.prototype.createFieldVectors = function () { + var fieldVectors = {}, + fieldRefs = Object.keys(this.fieldTermFrequencies), + fieldRefsLength = fieldRefs.length, + termIdfCache = Object.create(null) + + for (var i = 0; i < fieldRefsLength; i++) { + var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]), + fieldName = fieldRef.fieldName, + fieldLength = this.fieldLengths[fieldRef], + fieldVector = new lunr.Vector, + termFrequencies = this.fieldTermFrequencies[fieldRef], + terms = Object.keys(termFrequencies), + termsLength = terms.length + + + var fieldBoost = this._fields[fieldName].boost || 1, + docBoost = this._documents[fieldRef.docRef].boost || 1 + + for (var j = 0; j < termsLength; j++) { + var term = terms[j], + tf = termFrequencies[term], + termIndex = this.invertedIndex[term]._index, + idf, score, scoreWithPrecision + + if (termIdfCache[term] === undefined) { + idf = lunr.idf(this.invertedIndex[term], this.documentCount) + termIdfCache[term] = idf + } else { + idf = termIdfCache[term] + } + + score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf) + score *= fieldBoost + score *= docBoost + scoreWithPrecision = Math.round(score * 1000) / 1000 + // Converts 1.23456789 to 1.234. + // Reducing the precision so that the vectors take up less + // space when serialised. Doing it now so that they behave + // the same before and after serialisation. Also, this is + // the fastest approach to reducing a number's precision in + // JavaScript. + + fieldVector.insert(termIndex, scoreWithPrecision) + } + + fieldVectors[fieldRef] = fieldVector + } + + this.fieldVectors = fieldVectors +} + +/** + * Creates a token set of all tokens in the index using lunr.TokenSet + * + * @private + */ +lunr.Builder.prototype.createTokenSet = function () { + this.tokenSet = lunr.TokenSet.fromArray( + Object.keys(this.invertedIndex).sort() + ) +} + +/** + * Builds the index, creating an instance of lunr.Index. + * + * This completes the indexing process and should only be called + * once all documents have been added to the index. + * + * @returns {lunr.Index} + */ +lunr.Builder.prototype.build = function () { + this.calculateAverageFieldLengths() + this.createFieldVectors() + this.createTokenSet() + + return new lunr.Index({ + invertedIndex: this.invertedIndex, + fieldVectors: this.fieldVectors, + tokenSet: this.tokenSet, + fields: Object.keys(this._fields), + pipeline: this.searchPipeline + }) +} + +/** + * Applies a plugin to the index builder. + * + * A plugin is a function that is called with the index builder as its context. + * Plugins can be used to customise or extend the behaviour of the index + * in some way. A plugin is just a function, that encapsulated the custom + * behaviour that should be applied when building the index. + * + * The plugin function will be called with the index builder as its argument, additional + * arguments can also be passed when calling use. The function will be called + * with the index builder as its context. + * + * @param {Function} plugin The plugin to apply. + */ +lunr.Builder.prototype.use = function (fn) { + var args = Array.prototype.slice.call(arguments, 1) + args.unshift(this) + fn.apply(this, args) +} +/** + * Contains and collects metadata about a matching document. + * A single instance of lunr.MatchData is returned as part of every + * lunr.Index~Result. + * + * @constructor + * @param {string} term - The term this match data is associated with + * @param {string} field - The field in which the term was found + * @param {object} metadata - The metadata recorded about this term in this field + * @property {object} metadata - A cloned collection of metadata associated with this document. + * @see {@link lunr.Index~Result} + */ +lunr.MatchData = function (term, field, metadata) { + var clonedMetadata = Object.create(null), + metadataKeys = Object.keys(metadata || {}) + + // Cloning the metadata to prevent the original + // being mutated during match data combination. + // Metadata is kept in an array within the inverted + // index so cloning the data can be done with + // Array#slice + for (var i = 0; i < metadataKeys.length; i++) { + var key = metadataKeys[i] + clonedMetadata[key] = metadata[key].slice() + } + + this.metadata = Object.create(null) + + if (term !== undefined) { + this.metadata[term] = Object.create(null) + this.metadata[term][field] = clonedMetadata + } +} + +/** + * An instance of lunr.MatchData will be created for every term that matches a + * document. However only one instance is required in a lunr.Index~Result. This + * method combines metadata from another instance of lunr.MatchData with this + * objects metadata. + * + * @param {lunr.MatchData} otherMatchData - Another instance of match data to merge with this one. + * @see {@link lunr.Index~Result} + */ +lunr.MatchData.prototype.combine = function (otherMatchData) { + var terms = Object.keys(otherMatchData.metadata) + + for (var i = 0; i < terms.length; i++) { + var term = terms[i], + fields = Object.keys(otherMatchData.metadata[term]) + + if (this.metadata[term] == undefined) { + this.metadata[term] = Object.create(null) + } + + for (var j = 0; j < fields.length; j++) { + var field = fields[j], + keys = Object.keys(otherMatchData.metadata[term][field]) + + if (this.metadata[term][field] == undefined) { + this.metadata[term][field] = Object.create(null) + } + + for (var k = 0; k < keys.length; k++) { + var key = keys[k] + + if (this.metadata[term][field][key] == undefined) { + this.metadata[term][field][key] = otherMatchData.metadata[term][field][key] + } else { + this.metadata[term][field][key] = this.metadata[term][field][key].concat(otherMatchData.metadata[term][field][key]) + } + + } + } + } +} + +/** + * Add metadata for a term/field pair to this instance of match data. + * + * @param {string} term - The term this match data is associated with + * @param {string} field - The field in which the term was found + * @param {object} metadata - The metadata recorded about this term in this field + */ +lunr.MatchData.prototype.add = function (term, field, metadata) { + if (!(term in this.metadata)) { + this.metadata[term] = Object.create(null) + this.metadata[term][field] = metadata + return + } + + if (!(field in this.metadata[term])) { + this.metadata[term][field] = metadata + return + } + + var metadataKeys = Object.keys(metadata) + + for (var i = 0; i < metadataKeys.length; i++) { + var key = metadataKeys[i] + + if (key in this.metadata[term][field]) { + this.metadata[term][field][key] = this.metadata[term][field][key].concat(metadata[key]) + } else { + this.metadata[term][field][key] = metadata[key] + } + } +} +/** + * A lunr.Query provides a programmatic way of defining queries to be performed + * against a {@link lunr.Index}. + * + * Prefer constructing a lunr.Query using the {@link lunr.Index#query} method + * so the query object is pre-initialized with the right index fields. + * + * @constructor + * @property {lunr.Query~Clause[]} clauses - An array of query clauses. + * @property {string[]} allFields - An array of all available fields in a lunr.Index. + */ +lunr.Query = function (allFields) { + this.clauses = [] + this.allFields = allFields +} + +/** + * Constants for indicating what kind of automatic wildcard insertion will be used when constructing a query clause. + * + * This allows wildcards to be added to the beginning and end of a term without having to manually do any string + * concatenation. + * + * The wildcard constants can be bitwise combined to select both leading and trailing wildcards. + * + * @constant + * @default + * @property {number} wildcard.NONE - The term will have no wildcards inserted, this is the default behaviour + * @property {number} wildcard.LEADING - Prepend the term with a wildcard, unless a leading wildcard already exists + * @property {number} wildcard.TRAILING - Append a wildcard to the term, unless a trailing wildcard already exists + * @see lunr.Query~Clause + * @see lunr.Query#clause + * @see lunr.Query#term + * @example query term with trailing wildcard + * query.term('foo', { wildcard: lunr.Query.wildcard.TRAILING }) + * @example query term with leading and trailing wildcard + * query.term('foo', { + * wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING + * }) + */ + +lunr.Query.wildcard = new String ("*") +lunr.Query.wildcard.NONE = 0 +lunr.Query.wildcard.LEADING = 1 +lunr.Query.wildcard.TRAILING = 2 + +/** + * Constants for indicating what kind of presence a term must have in matching documents. + * + * @constant + * @enum {number} + * @see lunr.Query~Clause + * @see lunr.Query#clause + * @see lunr.Query#term + * @example query term with required presence + * query.term('foo', { presence: lunr.Query.presence.REQUIRED }) + */ +lunr.Query.presence = { + /** + * Term's presence in a document is optional, this is the default value. + */ + OPTIONAL: 1, + + /** + * Term's presence in a document is required, documents that do not contain + * this term will not be returned. + */ + REQUIRED: 2, + + /** + * Term's presence in a document is prohibited, documents that do contain + * this term will not be returned. + */ + PROHIBITED: 3 +} + +/** + * A single clause in a {@link lunr.Query} contains a term and details on how to + * match that term against a {@link lunr.Index}. + * + * @typedef {Object} lunr.Query~Clause + * @property {string[]} fields - The fields in an index this clause should be matched against. + * @property {number} [boost=1] - Any boost that should be applied when matching this clause. + * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be. + * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline. + * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended. + * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents. + */ + +/** + * Adds a {@link lunr.Query~Clause} to this query. + * + * Unless the clause contains the fields to be matched all fields will be matched. In addition + * a default boost of 1 is applied to the clause. + * + * @param {lunr.Query~Clause} clause - The clause to add to this query. + * @see lunr.Query~Clause + * @returns {lunr.Query} + */ +lunr.Query.prototype.clause = function (clause) { + if (!('fields' in clause)) { + clause.fields = this.allFields + } + + if (!('boost' in clause)) { + clause.boost = 1 + } + + if (!('usePipeline' in clause)) { + clause.usePipeline = true + } + + if (!('wildcard' in clause)) { + clause.wildcard = lunr.Query.wildcard.NONE + } + + if ((clause.wildcard & lunr.Query.wildcard.LEADING) && (clause.term.charAt(0) != lunr.Query.wildcard)) { + clause.term = "*" + clause.term + } + + if ((clause.wildcard & lunr.Query.wildcard.TRAILING) && (clause.term.slice(-1) != lunr.Query.wildcard)) { + clause.term = "" + clause.term + "*" + } + + if (!('presence' in clause)) { + clause.presence = lunr.Query.presence.OPTIONAL + } + + this.clauses.push(clause) + + return this +} + +/** + * A negated query is one in which every clause has a presence of + * prohibited. These queries require some special processing to return + * the expected results. + * + * @returns boolean + */ +lunr.Query.prototype.isNegated = function () { + for (var i = 0; i < this.clauses.length; i++) { + if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) { + return false + } + } + + return true +} + +/** + * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause} + * to the list of clauses that make up this query. + * + * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion + * to a token or token-like string should be done before calling this method. + * + * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an + * array, each term in the array will share the same options. + * + * @param {object|object[]} term - The term(s) to add to the query. + * @param {object} [options] - Any additional properties to add to the query clause. + * @returns {lunr.Query} + * @see lunr.Query#clause + * @see lunr.Query~Clause + * @example adding a single term to a query + * query.term("foo") + * @example adding a single term to a query and specifying search fields, term boost and automatic trailing wildcard + * query.term("foo", { + * fields: ["title"], + * boost: 10, + * wildcard: lunr.Query.wildcard.TRAILING + * }) + * @example using lunr.tokenizer to convert a string to tokens before using them as terms + * query.term(lunr.tokenizer("foo bar")) + */ +lunr.Query.prototype.term = function (term, options) { + if (Array.isArray(term)) { + term.forEach(function (t) { this.term(t, lunr.utils.clone(options)) }, this) + return this + } + + var clause = options || {} + clause.term = term.toString() + + this.clause(clause) + + return this +} +lunr.QueryParseError = function (message, start, end) { + this.name = "QueryParseError" + this.message = message + this.start = start + this.end = end +} + +lunr.QueryParseError.prototype = new Error +lunr.QueryLexer = function (str) { + this.lexemes = [] + this.str = str + this.length = str.length + this.pos = 0 + this.start = 0 + this.escapeCharPositions = [] +} + +lunr.QueryLexer.prototype.run = function () { + var state = lunr.QueryLexer.lexText + + while (state) { + state = state(this) + } +} + +lunr.QueryLexer.prototype.sliceString = function () { + var subSlices = [], + sliceStart = this.start, + sliceEnd = this.pos + + for (var i = 0; i < this.escapeCharPositions.length; i++) { + sliceEnd = this.escapeCharPositions[i] + subSlices.push(this.str.slice(sliceStart, sliceEnd)) + sliceStart = sliceEnd + 1 + } + + subSlices.push(this.str.slice(sliceStart, this.pos)) + this.escapeCharPositions.length = 0 + + return subSlices.join('') +} + +lunr.QueryLexer.prototype.emit = function (type) { + this.lexemes.push({ + type: type, + str: this.sliceString(), + start: this.start, + end: this.pos + }) + + this.start = this.pos +} + +lunr.QueryLexer.prototype.escapeCharacter = function () { + this.escapeCharPositions.push(this.pos - 1) + this.pos += 1 +} + +lunr.QueryLexer.prototype.next = function () { + if (this.pos >= this.length) { + return lunr.QueryLexer.EOS + } + + var char = this.str.charAt(this.pos) + this.pos += 1 + return char +} + +lunr.QueryLexer.prototype.width = function () { + return this.pos - this.start +} + +lunr.QueryLexer.prototype.ignore = function () { + if (this.start == this.pos) { + this.pos += 1 + } + + this.start = this.pos +} + +lunr.QueryLexer.prototype.backup = function () { + this.pos -= 1 +} + +lunr.QueryLexer.prototype.acceptDigitRun = function () { + var char, charCode + + do { + char = this.next() + charCode = char.charCodeAt(0) + } while (charCode > 47 && charCode < 58) + + if (char != lunr.QueryLexer.EOS) { + this.backup() + } +} + +lunr.QueryLexer.prototype.more = function () { + return this.pos < this.length +} + +lunr.QueryLexer.EOS = 'EOS' +lunr.QueryLexer.FIELD = 'FIELD' +lunr.QueryLexer.TERM = 'TERM' +lunr.QueryLexer.EDIT_DISTANCE = 'EDIT_DISTANCE' +lunr.QueryLexer.BOOST = 'BOOST' +lunr.QueryLexer.PRESENCE = 'PRESENCE' + +lunr.QueryLexer.lexField = function (lexer) { + lexer.backup() + lexer.emit(lunr.QueryLexer.FIELD) + lexer.ignore() + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexTerm = function (lexer) { + if (lexer.width() > 1) { + lexer.backup() + lexer.emit(lunr.QueryLexer.TERM) + } + + lexer.ignore() + + if (lexer.more()) { + return lunr.QueryLexer.lexText + } +} + +lunr.QueryLexer.lexEditDistance = function (lexer) { + lexer.ignore() + lexer.acceptDigitRun() + lexer.emit(lunr.QueryLexer.EDIT_DISTANCE) + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexBoost = function (lexer) { + lexer.ignore() + lexer.acceptDigitRun() + lexer.emit(lunr.QueryLexer.BOOST) + return lunr.QueryLexer.lexText +} + +lunr.QueryLexer.lexEOS = function (lexer) { + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } +} + +// This matches the separator used when tokenising fields +// within a document. These should match otherwise it is +// not possible to search for some tokens within a document. +// +// It is possible for the user to change the separator on the +// tokenizer so it _might_ clash with any other of the special +// characters already used within the search string, e.g. :. +// +// This means that it is possible to change the separator in +// such a way that makes some words unsearchable using a search +// string. +lunr.QueryLexer.termSeparator = lunr.tokenizer.separator + +lunr.QueryLexer.lexText = function (lexer) { + while (true) { + var char = lexer.next() + + if (char == lunr.QueryLexer.EOS) { + return lunr.QueryLexer.lexEOS + } + + // Escape character is '\' + if (char.charCodeAt(0) == 92) { + lexer.escapeCharacter() + continue + } + + if (char == ":") { + return lunr.QueryLexer.lexField + } + + if (char == "~") { + lexer.backup() + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } + return lunr.QueryLexer.lexEditDistance + } + + if (char == "^") { + lexer.backup() + if (lexer.width() > 0) { + lexer.emit(lunr.QueryLexer.TERM) + } + return lunr.QueryLexer.lexBoost + } + + // "+" indicates term presence is required + // checking for length to ensure that only + // leading "+" are considered + if (char == "+" && lexer.width() === 1) { + lexer.emit(lunr.QueryLexer.PRESENCE) + return lunr.QueryLexer.lexText + } + + // "-" indicates term presence is prohibited + // checking for length to ensure that only + // leading "-" are considered + if (char == "-" && lexer.width() === 1) { + lexer.emit(lunr.QueryLexer.PRESENCE) + return lunr.QueryLexer.lexText + } + + if (char.match(lunr.QueryLexer.termSeparator)) { + return lunr.QueryLexer.lexTerm + } + } +} + +lunr.QueryParser = function (str, query) { + this.lexer = new lunr.QueryLexer (str) + this.query = query + this.currentClause = {} + this.lexemeIdx = 0 +} + +lunr.QueryParser.prototype.parse = function () { + this.lexer.run() + this.lexemes = this.lexer.lexemes + + var state = lunr.QueryParser.parseClause + + while (state) { + state = state(this) + } + + return this.query +} + +lunr.QueryParser.prototype.peekLexeme = function () { + return this.lexemes[this.lexemeIdx] +} + +lunr.QueryParser.prototype.consumeLexeme = function () { + var lexeme = this.peekLexeme() + this.lexemeIdx += 1 + return lexeme +} + +lunr.QueryParser.prototype.nextClause = function () { + var completedClause = this.currentClause + this.query.clause(completedClause) + this.currentClause = {} +} + +lunr.QueryParser.parseClause = function (parser) { + var lexeme = parser.peekLexeme() + + if (lexeme == undefined) { + return + } + + switch (lexeme.type) { + case lunr.QueryLexer.PRESENCE: + return lunr.QueryParser.parsePresence + case lunr.QueryLexer.FIELD: + return lunr.QueryParser.parseField + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expected either a field or a term, found " + lexeme.type + + if (lexeme.str.length >= 1) { + errorMessage += " with value '" + lexeme.str + "'" + } + + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } +} + +lunr.QueryParser.parsePresence = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + switch (lexeme.str) { + case "-": + parser.currentClause.presence = lunr.Query.presence.PROHIBITED + break + case "+": + parser.currentClause.presence = lunr.Query.presence.REQUIRED + break + default: + var errorMessage = "unrecognised presence operator'" + lexeme.str + "'" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + var errorMessage = "expecting term or field, found nothing" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.FIELD: + return lunr.QueryParser.parseField + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expecting term or field, found '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseField = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + if (parser.query.allFields.indexOf(lexeme.str) == -1) { + var possibleFields = parser.query.allFields.map(function (f) { return "'" + f + "'" }).join(', '), + errorMessage = "unrecognised field '" + lexeme.str + "', possible fields: " + possibleFields + + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.fields = [lexeme.str] + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + var errorMessage = "expecting term, found nothing" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + return lunr.QueryParser.parseTerm + default: + var errorMessage = "expecting term, found '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseTerm = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + parser.currentClause.term = lexeme.str.toLowerCase() + + if (lexeme.str.indexOf("*") != -1) { + parser.currentClause.usePipeline = false + } + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseEditDistance = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + var editDistance = parseInt(lexeme.str, 10) + + if (isNaN(editDistance)) { + var errorMessage = "edit distance must be numeric" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.editDistance = editDistance + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + +lunr.QueryParser.parseBoost = function (parser) { + var lexeme = parser.consumeLexeme() + + if (lexeme == undefined) { + return + } + + var boost = parseInt(lexeme.str, 10) + + if (isNaN(boost)) { + var errorMessage = "boost must be numeric" + throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end) + } + + parser.currentClause.boost = boost + + var nextLexeme = parser.peekLexeme() + + if (nextLexeme == undefined) { + parser.nextClause() + return + } + + switch (nextLexeme.type) { + case lunr.QueryLexer.TERM: + parser.nextClause() + return lunr.QueryParser.parseTerm + case lunr.QueryLexer.FIELD: + parser.nextClause() + return lunr.QueryParser.parseField + case lunr.QueryLexer.EDIT_DISTANCE: + return lunr.QueryParser.parseEditDistance + case lunr.QueryLexer.BOOST: + return lunr.QueryParser.parseBoost + case lunr.QueryLexer.PRESENCE: + parser.nextClause() + return lunr.QueryParser.parsePresence + default: + var errorMessage = "Unexpected lexeme type '" + nextLexeme.type + "'" + throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end) + } +} + + /** + * export the module via AMD, CommonJS or as a browser global + * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js + */ + ;(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(factory) + } else if (typeof exports === 'object') { + /** + * Node. Does not work with strict CommonJS, but + * only CommonJS-like environments that support module.exports, + * like Node. + */ + module.exports = factory() + } else { + // Browser globals (root is window) + root.lunr = factory() + } + }(this, function () { + /** + * Just return a value to define the module export. + * This example returns an object, but the module + * can return a function as the exported value. + */ + return lunr + })) +})(); diff --git a/search/main.js b/search/main.js new file mode 100644 index 000000000..a5e469d7c --- /dev/null +++ b/search/main.js @@ -0,0 +1,109 @@ +function getSearchTermFromLocation() { + var sPageURL = window.location.search.substring(1); + var sURLVariables = sPageURL.split('&'); + for (var i = 0; i < sURLVariables.length; i++) { + var sParameterName = sURLVariables[i].split('='); + if (sParameterName[0] == 'q') { + return decodeURIComponent(sParameterName[1].replace(/\+/g, '%20')); + } + } +} + +function joinUrl (base, path) { + if (path.substring(0, 1) === "/") { + // path starts with `/`. Thus it is absolute. + return path; + } + if (base.substring(base.length-1) === "/") { + // base ends with `/` + return base + path; + } + return base + "/" + path; +} + +function escapeHtml (value) { + return value.replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); +} + +function formatResult (location, title, summary) { + return ''; +} + +function displayResults (results) { + var search_results = document.getElementById("mkdocs-search-results"); + while (search_results.firstChild) { + search_results.removeChild(search_results.firstChild); + } + if (results.length > 0){ + for (var i=0; i < results.length; i++){ + var result = results[i]; + var html = formatResult(result.location, result.title, result.summary); + search_results.insertAdjacentHTML('beforeend', html); + } + } else { + var noResultsText = search_results.getAttribute('data-no-results-text'); + if (!noResultsText) { + noResultsText = "No results found"; + } + search_results.insertAdjacentHTML('beforeend', '

' + noResultsText + '

'); + } +} + +function doSearch () { + var query = document.getElementById('mkdocs-search-query').value; + if (query.length > min_search_length) { + if (!window.Worker) { + displayResults(search(query)); + } else { + searchWorker.postMessage({query: query}); + } + } else { + // Clear results for short queries + displayResults([]); + } +} + +function initSearch () { + var search_input = document.getElementById('mkdocs-search-query'); + if (search_input) { + search_input.addEventListener("keyup", doSearch); + } + var term = getSearchTermFromLocation(); + if (term) { + search_input.value = term; + doSearch(); + } +} + +function onWorkerMessage (e) { + if (e.data.allowSearch) { + initSearch(); + } else if (e.data.results) { + var results = e.data.results; + displayResults(results); + } else if (e.data.config) { + min_search_length = e.data.config.min_search_length-1; + } +} + +if (!window.Worker) { + console.log('Web Worker API not supported'); + // load index in main thread + $.getScript(joinUrl(base_url, "search/worker.js")).done(function () { + console.log('Loaded worker'); + init(); + window.postMessage = function (msg) { + onWorkerMessage({data: msg}); + }; + }).fail(function (jqxhr, settings, exception) { + console.error('Could not load worker.js'); + }); +} else { + // Wrap search in a web worker + var searchWorker = new Worker(joinUrl(base_url, "search/worker.js")); + searchWorker.postMessage({init: true}); + searchWorker.onmessage = onWorkerMessage; +} diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 000000000..ddb90f04b --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"indexing":"full","lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"Welcome to Ubiq Ubiq is a Unity networking library, for research, teaching and development. Ubiq is built and maintained by the Virtual Environments and Computer Graphics group at University College London. Ubiq is 100% free and open source. Features Ubiq's goal is to enable your networked project. It includes message passing, room management, rendezvous and matchmaking, object spawning, shared binary blobs, multiple synchronisation models, lighweight XR interaction examples, customisable avatars and voice chat across Windows, Linux, Android, MacOS, and Javascript running in the browser. For Researchers Instructions for setting up your own server are included. Ubiq does not rely on any third-party services, making it GDPR-safe for your experiments.","title":"Home"},{"location":"#welcome-to-ubiq","text":"Ubiq is a Unity networking library, for research, teaching and development. Ubiq is built and maintained by the Virtual Environments and Computer Graphics group at University College London. Ubiq is 100% free and open source.","title":"Welcome to Ubiq"},{"location":"#features","text":"Ubiq's goal is to enable your networked project. It includes message passing, room management, rendezvous and matchmaking, object spawning, shared binary blobs, multiple synchronisation models, lighweight XR interaction examples, customisable avatars and voice chat across Windows, Linux, Android, MacOS, and Javascript running in the browser.","title":"Features"},{"location":"#for-researchers","text":"Instructions for setting up your own server are included. Ubiq does not rely on any third-party services, making it GDPR-safe for your experiments.","title":"For Researchers"},{"location":"3dpentutorial/","text":"Creating a 3D Pen for Ubiq In this short tutorial we'll make a pen that lets us draw shapes in mid-air. We'll add simple networking with Ubiq so you can share your drawings with others! 0) Download and install the Unity editor. We are using Unity 2020.3.40, but later versions should also work. 1) Create a new Unity project with the 3D template. 2) Download the Ubiq package for Unity (v0.3.0): release page , direct file link 3) Extract the Ubiq package into your new project's Packages folder. You should have the file structure: Packages/ubiq-0.3.0/Editor, as in the image below. 4) Open or return focus to your Unity editor and wait for Ubiq to be imported. 5) Open the Unity package manager with the top menu. The path is Window/Package Manager, highlighted in the image below. 6) In the package manager, select Ubiq from the list on the left. In the pane on the right, click to expand the Samples dropdown, then Import to load the Ubiq samples. Wait for the import to complete. 7) Open the Ubiq intro scene from the Samples: Assets/Samples/Ubiq/0.3.0/Samples/Start Here 8) Now we have everything we need, let's make a simple object for the pen. This can be anything you like! We will make a simple stand-in with one big cylinder for the grip and a tiny one for the nib. Right click in the hierarchy window and select Create Empty. Right click on the object in the hierarchy, select Rename, and give it the name \"3DPen\". This will be our parent object. We can use this to customize how our object is picked up. Now right click on 3DPen in the Hierarchy window and select 3D Object/Cylinder. Rename this cylinder \"Grip\". Right click on Grip and again select 3D Object/Cylinder. Rename this new cylinder \"Nib\". Your hierarchy should now read 3DPen/Grip/Nib, as in the image. 9) Scale, and translate the objects so they (sort of!) resemble a pen. Do not translate the top object (3DPen), as this will get moved when the user grabs it. Focus on just moving and scaling Grip and Nib. Do not worry about rotation now; we'll deal with this later. Tip: the scale of the sample is 1m equals 1 unit. Finally, position the pen somewhere you can easily reach it. You could try next to the menu. Here's what we ended up with: 10) Select 3DPen in the hierarchy window. Now, in the Inspector window, select Add Component and add a Rigidbody. In the Rigidbody, enable Is Kinematic. 11) Now let's write a script to help us pick up the pen and move it around. Select Add Component again, and type Pen. Unity will prompt you to add a new script with that name - select New Script, then Create and Add. 12) This will create the Pen script in your Assets folder and attach that script to the object. Open the file (Assets/Pen.cs) and replace its contents with the following: using UnityEngine; using Ubiq.XR; // Implement Graspable interface, part of Ubiq XR interaction // You can use any interaction toolkit you like with Ubiq! // For the sake of keeping this tutorial simple, we use our simple in-built // option. public class Pen : MonoBehaviour, IGraspable { private Hand controller; private void LateUpdate() { if (controller) { transform.position = controller.transform.position; transform.rotation = controller.transform.rotation; } } void IGraspable.Grasp(Hand controller) { this.controller = controller; } void IGraspable.Release(Hand controller) { this.controller = null; } } This implements the Graspable interface provided by Ubiq's XR interaction tools. You can use any interaction toolkit you like with Ubiq, but for the purpose of keeping this tutorial simple, we use our simple in-built option. 12) Enter Play mode by pressing the arrow at the top of the Unity Editor, or with the shortcut Ctrl-P. You can use Ubiq's desktop controls to walk over to the pen (WASD), look at it (hold right-click while moving the mouse), and grasp it (middle-mouse-button while the mouse is over the pen). When you move your view (as before, hold right-click while moving the mouse) again, the object should move with it. 13) Let's also build your application to test networking functionality. First, go to the top bar, then Edit/Project Settings. In the Project Settings window, select Player from the list on the left. In the pane on the right, click the dropdown next to Fullscreen Mode and select Windowed, then set the window to something small, like 640 x 480. This helps us test because we can see both the editor and the application running in a small window. Now, again in the top bar, go to File/Build and Run. Select a location for the build, and wait for the build to complete. 14) Now your application should be running in both the editor and as a windowed standalone app. To connect the two, we'll need them to both join the same room. On the editor, use your mouse to click the New button on the Ubiq UI panel in the scene. Leave the name as default, and click the arrow at the top right of the UI panel. Finally, select \"No, keep my room private\". The panel will change to show you a three letter code. This is the 'joincode' for your room. In your standalone windowed application, click Join on the Ubiq sample UI, enter this code, then click the arrow to submit. 15) Now you have two applications, both connected to the same room. On both, you should now see another avatar in the room with you. Try picking up the object as in Step 12. You'll see that it moves for the user who picked it up, but in the other application it stays still. This means we need to add some networking! 16) Replace your Pen script (Assets/Pen.cs) with the following: using UnityEngine; using Ubiq.XR; using Ubiq.Messaging; // new public class Pen : MonoBehaviour, IGraspable { private NetworkContext context; // new private bool owner; // new private Hand controller; // new // 1. Define a message format. Let's us know what to expect on send and recv private struct Message { public Vector3 position; public Quaternion rotation; public Message(Transform transform) { this.position = transform.position; this.rotation = transform.rotation; } } // new private void Start() { // 2. Register the object with the network scene. This provides a // NetworkID for the object and lets it get messages from remote users context = NetworkScene.Register(this); } // new public void ProcessMessage (ReferenceCountedSceneGraphMessage msg) { // 3. Receive and use transform update messages from remote users // Here we use them to update our current position var data = msg.FromJson(); transform.position = data.position; transform.rotation = data.rotation; } // new private void FixedUpdate() { if (owner) { // 4. Send transform update messages if we are the current 'owner' context.SendJson(new Message(transform)); } } private void LateUpdate() { if (controller) { transform.position = controller.transform.position; transform.rotation = controller.transform.rotation; } } void IGraspable.Grasp(Hand controller) { // 5. Define ownership as 'who holds the item currently' owner = true; // new this.controller = controller; } void IGraspable.Release(Hand controller) { // As 5. above, define ownership as 'who holds the item currently' owner = false; // new this.controller = null; } // Note about ownership: 'ownership' is just one way of designing this // kind of script. It's sometimes a useful pattern, but has no special // significance outside of this file or in Ubiq more generally. } New lines are functioned are marked with a comment. This script does a number of important things, marked in the code. 17) Test again as described in Steps 12-14. You should now see that when the object is grasped and moved in one application, it also moves in the other! 18) Now let's get the pen to draw in 3D space! Replace Pen.cs with the following: using UnityEngine; using Ubiq.XR; using Ubiq.Messaging; public class Pen : MonoBehaviour, IGraspable, IUseable // new { private NetworkContext context; private bool owner; private Hand controller; private Transform nib; // new private Material drawingMaterial; // new private GameObject currentDrawing; // new private struct Message { public Vector3 position; public Quaternion rotation; public Message(Transform transform) { this.position = transform.position; this.rotation = transform.rotation; } } private void Start() { nib = transform.Find(\"Grip/Nib\"); // new context = NetworkScene.Register(this); var shader = Shader.Find(\"Particles/Standard Unlit\"); // new drawingMaterial = new Material(shader); // new } public void ProcessMessage (ReferenceCountedSceneGraphMessage msg) { var data = msg.FromJson(); transform.position = data.position; transform.rotation = data.rotation; } private void FixedUpdate() { if (owner) { context.SendJson(new Message(transform)); } } private void LateUpdate() { if (controller) { transform.position = controller.transform.position; transform.rotation = controller.transform.rotation; } } void IGraspable.Grasp(Hand controller) { owner = true; this.controller = controller; } void IGraspable.Release(Hand controller) { owner = false; this.controller = null; } // new void IUseable.Use(Hand controller) { BeginDrawing(); } // new void IUseable.UnUse(Hand controller) { EndDrawing(); } // new private void BeginDrawing() { currentDrawing = new GameObject(\"Drawing\"); var trail = currentDrawing.AddComponent(); trail.time = Mathf.Infinity; trail.material = drawingMaterial; trail.startWidth = .05f; trail.endWidth = .05f; trail.minVertexDistance = .02f; currentDrawing.transform.parent = nib.transform; currentDrawing.transform.localPosition = Vector3.zero; currentDrawing.transform.localRotation = Quaternion.identity; } // new private void EndDrawing() { var trail = currentDrawing.GetComponent(); currentDrawing.transform.parent = null; currentDrawing.GetComponent().emitting = false; currentDrawing = null; } } 19) Test as in steps 12-14. You should now be able to draw a line in the air with the pen! This is intuitive in virtual reality - pick up the item and grasp buttons/triggers, and use with main button/trigger. The desktop interface is fiddly for this, but okay for debug: First, click on the pen to 'use' it - you should see a debug message in the Unity editor if successful. Then, while still holding left mouse to use, hold right mouse to move your view around. But you'll notice that the line is only drawn locally so far - the remote user does not yet see it. We'll change that in the next step. 20) Time to add networking to our drawings! Replace Pen.cs with this final version: using UnityEngine; using Ubiq.XR; using Ubiq.Messaging; // Adds simple networking to the 3d pen. The approach used is to draw locally // when a remote user tells us they are drawing, and stop drawing locally when // a remote user tells us they are not. public class Pen : MonoBehaviour, IGraspable, IUseable { private NetworkContext context; private bool owner; private Hand controller; private Transform nib; private Material drawingMaterial; private GameObject currentDrawing; // Amend message to also store current drawing state private struct Message { public Vector3 position; public Quaternion rotation; public bool isDrawing; // new public Message(Transform transform, bool isDrawing) { this.position = transform.position; this.rotation = transform.rotation; this.isDrawing = isDrawing; // new } } private void Start() { nib = transform.Find(\"Grip/Nib\"); context = NetworkScene.Register(this); var shader = Shader.Find(\"Particles/Standard Unlit\"); drawingMaterial = new Material(shader); } public void ProcessMessage (ReferenceCountedSceneGraphMessage msg) { var data = msg.FromJson(); transform.position = data.position; transform.rotation = data.rotation; // new // Also start drawing locally when a remote user starts if (data.isDrawing && !currentDrawing) { BeginDrawing(); } if (!data.isDrawing && currentDrawing) { EndDrawing(); } } private void FixedUpdate() { if (owner) { // new context.SendJson(new Message(transform,isDrawing:currentDrawing)); } } private void LateUpdate() { if (controller) { transform.position = controller.transform.position; transform.rotation = controller.transform.rotation; } } void IGraspable.Grasp(Hand controller) { owner = true; this.controller = controller; } void IGraspable.Release(Hand controller) { owner = false; this.controller = null; } void IUseable.Use(Hand controller) { BeginDrawing(); } void IUseable.UnUse(Hand controller) { EndDrawing(); } private void BeginDrawing() { currentDrawing = new GameObject(\"Drawing\"); var trail = currentDrawing.AddComponent(); trail.time = Mathf.Infinity; trail.material = drawingMaterial; trail.startWidth = .05f; trail.endWidth = .05f; trail.minVertexDistance = .02f; currentDrawing.transform.parent = nib.transform; currentDrawing.transform.localPosition = Vector3.zero; currentDrawing.transform.localRotation = Quaternion.identity; } private void EndDrawing() { var trail = currentDrawing.GetComponent(); currentDrawing.transform.parent = null; currentDrawing.GetComponent().emitting = false; currentDrawing = null; } } And we're done! Test it again as with steps 12-14, and if you have a headset available, see how it feels in VR! You might notice that drawings are not visible to new joining users. A more advanced implementation would store the points of the drawings in Peer or Room properties, so new users could see them when they join. If you do try it, let us know how you get on!","title":"Creating a 3D Pen"},{"location":"3dpentutorial/#creating-a-3d-pen-for-ubiq","text":"In this short tutorial we'll make a pen that lets us draw shapes in mid-air. We'll add simple networking with Ubiq so you can share your drawings with others! 0) Download and install the Unity editor. We are using Unity 2020.3.40, but later versions should also work. 1) Create a new Unity project with the 3D template. 2) Download the Ubiq package for Unity (v0.3.0): release page , direct file link 3) Extract the Ubiq package into your new project's Packages folder. You should have the file structure: Packages/ubiq-0.3.0/Editor, as in the image below. 4) Open or return focus to your Unity editor and wait for Ubiq to be imported. 5) Open the Unity package manager with the top menu. The path is Window/Package Manager, highlighted in the image below. 6) In the package manager, select Ubiq from the list on the left. In the pane on the right, click to expand the Samples dropdown, then Import to load the Ubiq samples. Wait for the import to complete. 7) Open the Ubiq intro scene from the Samples: Assets/Samples/Ubiq/0.3.0/Samples/Start Here 8) Now we have everything we need, let's make a simple object for the pen. This can be anything you like! We will make a simple stand-in with one big cylinder for the grip and a tiny one for the nib. Right click in the hierarchy window and select Create Empty. Right click on the object in the hierarchy, select Rename, and give it the name \"3DPen\". This will be our parent object. We can use this to customize how our object is picked up. Now right click on 3DPen in the Hierarchy window and select 3D Object/Cylinder. Rename this cylinder \"Grip\". Right click on Grip and again select 3D Object/Cylinder. Rename this new cylinder \"Nib\". Your hierarchy should now read 3DPen/Grip/Nib, as in the image. 9) Scale, and translate the objects so they (sort of!) resemble a pen. Do not translate the top object (3DPen), as this will get moved when the user grabs it. Focus on just moving and scaling Grip and Nib. Do not worry about rotation now; we'll deal with this later. Tip: the scale of the sample is 1m equals 1 unit. Finally, position the pen somewhere you can easily reach it. You could try next to the menu. Here's what we ended up with: 10) Select 3DPen in the hierarchy window. Now, in the Inspector window, select Add Component and add a Rigidbody. In the Rigidbody, enable Is Kinematic. 11) Now let's write a script to help us pick up the pen and move it around. Select Add Component again, and type Pen. Unity will prompt you to add a new script with that name - select New Script, then Create and Add. 12) This will create the Pen script in your Assets folder and attach that script to the object. Open the file (Assets/Pen.cs) and replace its contents with the following: using UnityEngine; using Ubiq.XR; // Implement Graspable interface, part of Ubiq XR interaction // You can use any interaction toolkit you like with Ubiq! // For the sake of keeping this tutorial simple, we use our simple in-built // option. public class Pen : MonoBehaviour, IGraspable { private Hand controller; private void LateUpdate() { if (controller) { transform.position = controller.transform.position; transform.rotation = controller.transform.rotation; } } void IGraspable.Grasp(Hand controller) { this.controller = controller; } void IGraspable.Release(Hand controller) { this.controller = null; } } This implements the Graspable interface provided by Ubiq's XR interaction tools. You can use any interaction toolkit you like with Ubiq, but for the purpose of keeping this tutorial simple, we use our simple in-built option. 12) Enter Play mode by pressing the arrow at the top of the Unity Editor, or with the shortcut Ctrl-P. You can use Ubiq's desktop controls to walk over to the pen (WASD), look at it (hold right-click while moving the mouse), and grasp it (middle-mouse-button while the mouse is over the pen). When you move your view (as before, hold right-click while moving the mouse) again, the object should move with it. 13) Let's also build your application to test networking functionality. First, go to the top bar, then Edit/Project Settings. In the Project Settings window, select Player from the list on the left. In the pane on the right, click the dropdown next to Fullscreen Mode and select Windowed, then set the window to something small, like 640 x 480. This helps us test because we can see both the editor and the application running in a small window. Now, again in the top bar, go to File/Build and Run. Select a location for the build, and wait for the build to complete. 14) Now your application should be running in both the editor and as a windowed standalone app. To connect the two, we'll need them to both join the same room. On the editor, use your mouse to click the New button on the Ubiq UI panel in the scene. Leave the name as default, and click the arrow at the top right of the UI panel. Finally, select \"No, keep my room private\". The panel will change to show you a three letter code. This is the 'joincode' for your room. In your standalone windowed application, click Join on the Ubiq sample UI, enter this code, then click the arrow to submit. 15) Now you have two applications, both connected to the same room. On both, you should now see another avatar in the room with you. Try picking up the object as in Step 12. You'll see that it moves for the user who picked it up, but in the other application it stays still. This means we need to add some networking! 16) Replace your Pen script (Assets/Pen.cs) with the following: using UnityEngine; using Ubiq.XR; using Ubiq.Messaging; // new public class Pen : MonoBehaviour, IGraspable { private NetworkContext context; // new private bool owner; // new private Hand controller; // new // 1. Define a message format. Let's us know what to expect on send and recv private struct Message { public Vector3 position; public Quaternion rotation; public Message(Transform transform) { this.position = transform.position; this.rotation = transform.rotation; } } // new private void Start() { // 2. Register the object with the network scene. This provides a // NetworkID for the object and lets it get messages from remote users context = NetworkScene.Register(this); } // new public void ProcessMessage (ReferenceCountedSceneGraphMessage msg) { // 3. Receive and use transform update messages from remote users // Here we use them to update our current position var data = msg.FromJson(); transform.position = data.position; transform.rotation = data.rotation; } // new private void FixedUpdate() { if (owner) { // 4. Send transform update messages if we are the current 'owner' context.SendJson(new Message(transform)); } } private void LateUpdate() { if (controller) { transform.position = controller.transform.position; transform.rotation = controller.transform.rotation; } } void IGraspable.Grasp(Hand controller) { // 5. Define ownership as 'who holds the item currently' owner = true; // new this.controller = controller; } void IGraspable.Release(Hand controller) { // As 5. above, define ownership as 'who holds the item currently' owner = false; // new this.controller = null; } // Note about ownership: 'ownership' is just one way of designing this // kind of script. It's sometimes a useful pattern, but has no special // significance outside of this file or in Ubiq more generally. } New lines are functioned are marked with a comment. This script does a number of important things, marked in the code. 17) Test again as described in Steps 12-14. You should now see that when the object is grasped and moved in one application, it also moves in the other! 18) Now let's get the pen to draw in 3D space! Replace Pen.cs with the following: using UnityEngine; using Ubiq.XR; using Ubiq.Messaging; public class Pen : MonoBehaviour, IGraspable, IUseable // new { private NetworkContext context; private bool owner; private Hand controller; private Transform nib; // new private Material drawingMaterial; // new private GameObject currentDrawing; // new private struct Message { public Vector3 position; public Quaternion rotation; public Message(Transform transform) { this.position = transform.position; this.rotation = transform.rotation; } } private void Start() { nib = transform.Find(\"Grip/Nib\"); // new context = NetworkScene.Register(this); var shader = Shader.Find(\"Particles/Standard Unlit\"); // new drawingMaterial = new Material(shader); // new } public void ProcessMessage (ReferenceCountedSceneGraphMessage msg) { var data = msg.FromJson(); transform.position = data.position; transform.rotation = data.rotation; } private void FixedUpdate() { if (owner) { context.SendJson(new Message(transform)); } } private void LateUpdate() { if (controller) { transform.position = controller.transform.position; transform.rotation = controller.transform.rotation; } } void IGraspable.Grasp(Hand controller) { owner = true; this.controller = controller; } void IGraspable.Release(Hand controller) { owner = false; this.controller = null; } // new void IUseable.Use(Hand controller) { BeginDrawing(); } // new void IUseable.UnUse(Hand controller) { EndDrawing(); } // new private void BeginDrawing() { currentDrawing = new GameObject(\"Drawing\"); var trail = currentDrawing.AddComponent(); trail.time = Mathf.Infinity; trail.material = drawingMaterial; trail.startWidth = .05f; trail.endWidth = .05f; trail.minVertexDistance = .02f; currentDrawing.transform.parent = nib.transform; currentDrawing.transform.localPosition = Vector3.zero; currentDrawing.transform.localRotation = Quaternion.identity; } // new private void EndDrawing() { var trail = currentDrawing.GetComponent(); currentDrawing.transform.parent = null; currentDrawing.GetComponent().emitting = false; currentDrawing = null; } } 19) Test as in steps 12-14. You should now be able to draw a line in the air with the pen! This is intuitive in virtual reality - pick up the item and grasp buttons/triggers, and use with main button/trigger. The desktop interface is fiddly for this, but okay for debug: First, click on the pen to 'use' it - you should see a debug message in the Unity editor if successful. Then, while still holding left mouse to use, hold right mouse to move your view around. But you'll notice that the line is only drawn locally so far - the remote user does not yet see it. We'll change that in the next step. 20) Time to add networking to our drawings! Replace Pen.cs with this final version: using UnityEngine; using Ubiq.XR; using Ubiq.Messaging; // Adds simple networking to the 3d pen. The approach used is to draw locally // when a remote user tells us they are drawing, and stop drawing locally when // a remote user tells us they are not. public class Pen : MonoBehaviour, IGraspable, IUseable { private NetworkContext context; private bool owner; private Hand controller; private Transform nib; private Material drawingMaterial; private GameObject currentDrawing; // Amend message to also store current drawing state private struct Message { public Vector3 position; public Quaternion rotation; public bool isDrawing; // new public Message(Transform transform, bool isDrawing) { this.position = transform.position; this.rotation = transform.rotation; this.isDrawing = isDrawing; // new } } private void Start() { nib = transform.Find(\"Grip/Nib\"); context = NetworkScene.Register(this); var shader = Shader.Find(\"Particles/Standard Unlit\"); drawingMaterial = new Material(shader); } public void ProcessMessage (ReferenceCountedSceneGraphMessage msg) { var data = msg.FromJson(); transform.position = data.position; transform.rotation = data.rotation; // new // Also start drawing locally when a remote user starts if (data.isDrawing && !currentDrawing) { BeginDrawing(); } if (!data.isDrawing && currentDrawing) { EndDrawing(); } } private void FixedUpdate() { if (owner) { // new context.SendJson(new Message(transform,isDrawing:currentDrawing)); } } private void LateUpdate() { if (controller) { transform.position = controller.transform.position; transform.rotation = controller.transform.rotation; } } void IGraspable.Grasp(Hand controller) { owner = true; this.controller = controller; } void IGraspable.Release(Hand controller) { owner = false; this.controller = null; } void IUseable.Use(Hand controller) { BeginDrawing(); } void IUseable.UnUse(Hand controller) { EndDrawing(); } private void BeginDrawing() { currentDrawing = new GameObject(\"Drawing\"); var trail = currentDrawing.AddComponent(); trail.time = Mathf.Infinity; trail.material = drawingMaterial; trail.startWidth = .05f; trail.endWidth = .05f; trail.minVertexDistance = .02f; currentDrawing.transform.parent = nib.transform; currentDrawing.transform.localPosition = Vector3.zero; currentDrawing.transform.localRotation = Quaternion.identity; } private void EndDrawing() { var trail = currentDrawing.GetComponent(); currentDrawing.transform.parent = null; currentDrawing.GetComponent().emitting = false; currentDrawing = null; } } And we're done! Test it again as with steps 12-14, and if you have a headset available, see how it feels in VR! You might notice that drawings are not visible to new joining users. A more advanced implementation would store the points of the drawings in Peer or Room properties, so new users could see them when they join. If you do try it, let us know how you get on!","title":"Creating a 3D Pen for Ubiq"},{"location":"asyncdesignpatterns/","text":"Asyncrhonous Design Patterns in Unity The Unity process manages the main thread, which begins before any user code is executed. Most Unity resources can only be accessed from the main thread; an exception will be thrown otherwise. There are still many possibilities for writing aysnchronous code however. Design Pattern Delayed initialisation with callbacks. Mimics the do-then pattern in JS. Methods are called which take Actions. Those Actions are initialised by lambdas. The lambas execution thread depends on the called function. void Start() { factory.GetRtcConfiguration(config => { pc = factory.CreatePeerConnection(config, this); }); } Design Pattern Message pumps with Update. Commonly used in the mid-level networking code, this pattern uses a list of actions to execute methods on the main thread. class RoomClient { private List actions = new List(); public void SendToServer(Message message) { actions.Add(() => { SendToServerSync(message); }); } private void Update() { foreach (var action in actions) { action(); } actions.Clear(); } } Design Pattern Commonly used in webrtc code for objects that take time to initialise because they are waiting on external resources. This pattern uses coroutines to effectively poll a resource, conditionally executing operations on the main thread. void Start() { factory.GetRtcConfiguration(config => { pc = factory.CreatePeerConnection(config, this); }); } private IEnumerator WaitForPeerConnection(Action OnPcCreated) { while (pc == null) { yield return null; } OnPcCreated(); } public void AddLocalAudioSource() { StartCoroutine(WaitForPeerConnection(() => { var audiosource = factory.CreateAudioSource(); var audiotrack = factory.CreateAudioTrack(\"localAudioSource\", audiosource); pc.AddTrack(audiotrack, new[] { \"localAudioSource\" }); })); }","title":"Async Design Patterns"},{"location":"asyncdesignpatterns/#asyncrhonous-design-patterns-in-unity","text":"The Unity process manages the main thread, which begins before any user code is executed. Most Unity resources can only be accessed from the main thread; an exception will be thrown otherwise. There are still many possibilities for writing aysnchronous code however.","title":"Asyncrhonous Design Patterns in Unity"},{"location":"asyncdesignpatterns/#design-pattern","text":"Delayed initialisation with callbacks. Mimics the do-then pattern in JS. Methods are called which take Actions. Those Actions are initialised by lambdas. The lambas execution thread depends on the called function. void Start() { factory.GetRtcConfiguration(config => { pc = factory.CreatePeerConnection(config, this); }); }","title":"Design Pattern"},{"location":"asyncdesignpatterns/#design-pattern_1","text":"Message pumps with Update. Commonly used in the mid-level networking code, this pattern uses a list of actions to execute methods on the main thread. class RoomClient { private List actions = new List(); public void SendToServer(Message message) { actions.Add(() => { SendToServerSync(message); }); } private void Update() { foreach (var action in actions) { action(); } actions.Clear(); } }","title":"Design Pattern"},{"location":"asyncdesignpatterns/#design-pattern_2","text":"Commonly used in webrtc code for objects that take time to initialise because they are waiting on external resources. This pattern uses coroutines to effectively poll a resource, conditionally executing operations on the main thread. void Start() { factory.GetRtcConfiguration(config => { pc = factory.CreatePeerConnection(config, this); }); } private IEnumerator WaitForPeerConnection(Action OnPcCreated) { while (pc == null) { yield return null; } OnPcCreated(); } public void AddLocalAudioSource() { StartCoroutine(WaitForPeerConnection(() => { var audiosource = factory.CreateAudioSource(); var audiotrack = factory.CreateAudioTrack(\"localAudioSource\", audiosource); pc.AddTrack(audiotrack, new[] { \"localAudioSource\" }); })); }","title":"Design Pattern"},{"location":"botssample/","text":"Bots Ubiq has the ability to simulate large numbers of users. This may be useful when stress testing, for example. This is demonstrated in the Bots Application Sample. Bot Peer In a regular application, a user interacts with the world through the PlayerController . They connect to the network using a NetworkScene and become a Ubiq Peer . It is the same with the Bot and Bot Peer. The Bot GameObject contains Components which interact with the world. Pairing a Bot with a NetworkScene creates a Bot Peer , that can connect to a room and act autonomously. The Bot Peer is equivalent to a user application. In the Editor, the Bot Peer can be joined to a room by using the RoomClient Editor Controls, just like a regular Peer. Alternatively, the default Bot, and others, can be controlled en-masse using the Bots Controller. The Bot Peer Prefab can be dropped into new scenes and controlled via the Editor, where small numbers of Bots are required. Bot The Bot Prefab implements the behaviour of the non-player controlled character. The sample Bot contains a number of common abilities, which can be extended by adding additional custom Components. Avatar The Bot Peer contains a skeleton allowing it to embody an Avatar at remote peers. Behaviour The Bot Peer Avatar is controlled by the Bot Component. This uses a nav mesh to move between random points in the environment. Audio The Bot Peer can speak with a pre-recorded audio clip. This is through a Component that takes the place of the Microphone input in a player-controlled Peer. Managing Multiple Bots Usually, a single Unity process is a single Ubiq Peer, with one NetworkScene. This is because a single PC can usually only drive one set of user input devices at a time. With bots, a single process can host multiple Bots, but each Bot is still a separate Peer, and has its own NetworkScene. This is achived using the same scene-graph Forests as the Local Loopback scene. BotsManager & BotsController The Bots Application Sample contains Components to create and manage large numbers of bots. The BotsManager is the Component that creates and configures Bot Peers within a single process. Bots Managers are controlled remotely by a BotsController . The BotsController and BotsManager use Ubiq to communicate. The BotsManager and BotsController join the same Command and Control Room . The Bot Peers themselves join different rooms, possibly even on different servers. The BotManager communicates with the Bot Peers through the local scene graph. Multiple Bot Managers across different processes can be controlled this way, each managing a number of Bots. A typical Unity process running on a modern desktop can handle approximately 20 bots. All three Components can run within one scene graph, within one process, as in the Bots Sample Scene, or can be split between multiple machines to control hundreds of bots. Creating Bots with the Bots Scene Opening the Bots scene and pressing Play will show a birds-eye view of the local process, and a control panel. This scene is only meant to be used on the desktop. The Bots scene already has one Bot Peer created. This GameObject's NetworkScene Editor Controls or the Create Room/Join Room Control Panel Buttons can be used to immediately join the Bot into a room with other Peers - bots or normal Peers. Stress Testing with the Bots scene One of the uses for Bots is stress testing. The Bots Sample is set up to show this. The Bots Scene contains a number of Components, and a UI. Bots Control This GameObject contains resources to allow a user to control one or more Bot Managers with the UI. It also includes the Camera and Event Manager. The UI contained under Bots Control drives the BotsController , which in turn directs Bot Managers. The BotsController is in the Control Room GameObject, which has its own NetworkScene and so forms the Bots Controller Peer. This branch also contains a Peer (Bots Room Peer) that can be connected to the Bots' room, in order to do things such as take statistics or collect logs. Bots Manager The Bots Manager GameObject is the forest for the Bots Manager Peer. Though they are in the same scene graph, the Bots Manager Peer and Bots Controller Peer both join the same command and control room. Environment To reduce memory overhead, multiple Bot Peers share the same Environment, though as they have their own NetworkScene they don't directly interact outside of Ubiq networking. Bot Peer 1 The Scene includes one Bot Peer instance. The Control Panel can be used to add more. Bots Config The Bots Config GameObject hosts a helper Component to set which servers the Control Room and Bots Room should be hosted on at design time. By allowing these to be set at design time, the Bots Scene can be built to an executable to be run headless on different machines without needing further configuration. Make sure to change the Control Room Id before running the sample against Nexus, in case others are running the same sample. Controlling Bots The Bots Manager is used to spawn new Bot Peer instances. Bot Peers exist in the same application but are completley independent as far as the network, and other peers, are concerned. The UI in the Bots scene is for the Bot Controller. There is no interface for interacting with the virtual world, VR or otherwise, as there is no Network Scene for players in the Bots example. Bots can be instructed to join any room however, including those with regular players. Command and Control and Bots Rooms When the Scene is started, the Control Panel UI will control the local Bot Manager. However, there is a limit to how many Bot Peers a single Unity process can host. Bot Managers across different Unity processes can work together to control very large numbers of bots. The Control Panel and Bot Manager are two distinct Ubiq Peers. They communicate using Ubiq Messages through a 'Command and Control' Room. When the Control Panel starts, it creates a new Room and has the local Bot Manager join it. Additional Bot Managers from other processes can join this room too, and fall under the control of the Control Panel. The Command and Control Room can be any Ubiq Room, including the one that the Bots join. The Room can be set via the command line with the -commandroomjoincode argument. This, combined with -batchmode, can allow many headless Unity instances to spawn bots. In practice though, when doing things like stress testing, the Room should be different, even on a different server if doing server stress testing. Servers There are in total four distinct Peers in the single Bots Sample: the Bot Controller (Control Room), the Bot Manager (Control Room), the Bot Room Peer (Bots Room) and the default Bot Peer (Bots Room). The Bots Config Component is used to change the server(s) for all of these at once. The default server for Bot Peer 1 is set in the standard NetworkScene Prefab; it is overridden before connecting when the Control Panel is used to control the Bots. Control Panel The UI has two sets of controls: Common controls and Instance controls. Common Controls apply to all Bot Managers, and Instance controls apply to just that Bot Manager. When only one Bot Manager instance is known, the controls behave identically. Toggle Camera As each Peer acts as if it were the only one in the process, each Peer will show all other Peers Avatars. This can create high rendering loads. Avatars can be hidden by toggling the Camera, which changes the Culling Mask. The camera is not completley disabled, as it is needed for the UI. Enable Audio Enables or Disables the audio chat channel for new Bots. This can be used to reduce compute load on the clients. This may be desireable if stress testing a server, for example, as audio data doesn't pass through the server. Create/Join Room The Create Room button creates a new room for Bots , and commands any existing Bots to join it. Alternatively, an existing Join Code can be entered and the Join Room button used. Either can be used as many times as desired to change all the Bots at once, without re-creating the Bots. Number of Peers Shows the number of Peers in the Bots Room, including the 'dummy' peer. A process may host a number of Bots that have not yet joined a room. Use this figure to monitor the actual number of bots in a room together. Bot Manager Instances Below the Common Controls each Bot Manager that the Controller is aware of is listed. Each line shows the Id of the Manager, as well as a the number of bots it is hosting. The Input Field and Add Bot Button can be used add new Bots to that instance. If a Bots Room has been set up, new Bots will automatically join it. The FPS is used to approximate the performance or load of the Bots Manager. The Colour is controlled by the Fps Gradient member of the Bots Manager Control Prefab (between 0-100 Fps). Bot Scene The Bots Application Sample also contains a sample scene with a single Bot Peer, along with a Player Peer. This can be used to see what it is like for players to interact with Bots. In this scene, the Player and the Bot should be joined to the same room manually using the RoomClient Editor controls.","title":"Bots"},{"location":"botssample/#bots","text":"Ubiq has the ability to simulate large numbers of users. This may be useful when stress testing, for example. This is demonstrated in the Bots Application Sample.","title":"Bots"},{"location":"botssample/#bot-peer","text":"In a regular application, a user interacts with the world through the PlayerController . They connect to the network using a NetworkScene and become a Ubiq Peer . It is the same with the Bot and Bot Peer. The Bot GameObject contains Components which interact with the world. Pairing a Bot with a NetworkScene creates a Bot Peer , that can connect to a room and act autonomously. The Bot Peer is equivalent to a user application. In the Editor, the Bot Peer can be joined to a room by using the RoomClient Editor Controls, just like a regular Peer. Alternatively, the default Bot, and others, can be controlled en-masse using the Bots Controller. The Bot Peer Prefab can be dropped into new scenes and controlled via the Editor, where small numbers of Bots are required.","title":"Bot Peer"},{"location":"botssample/#bot","text":"The Bot Prefab implements the behaviour of the non-player controlled character. The sample Bot contains a number of common abilities, which can be extended by adding additional custom Components. Avatar The Bot Peer contains a skeleton allowing it to embody an Avatar at remote peers. Behaviour The Bot Peer Avatar is controlled by the Bot Component. This uses a nav mesh to move between random points in the environment. Audio The Bot Peer can speak with a pre-recorded audio clip. This is through a Component that takes the place of the Microphone input in a player-controlled Peer.","title":"Bot"},{"location":"botssample/#managing-multiple-bots","text":"Usually, a single Unity process is a single Ubiq Peer, with one NetworkScene. This is because a single PC can usually only drive one set of user input devices at a time. With bots, a single process can host multiple Bots, but each Bot is still a separate Peer, and has its own NetworkScene. This is achived using the same scene-graph Forests as the Local Loopback scene.","title":"Managing Multiple Bots"},{"location":"botssample/#botsmanager-botscontroller","text":"The Bots Application Sample contains Components to create and manage large numbers of bots. The BotsManager is the Component that creates and configures Bot Peers within a single process. Bots Managers are controlled remotely by a BotsController . The BotsController and BotsManager use Ubiq to communicate. The BotsManager and BotsController join the same Command and Control Room . The Bot Peers themselves join different rooms, possibly even on different servers. The BotManager communicates with the Bot Peers through the local scene graph. Multiple Bot Managers across different processes can be controlled this way, each managing a number of Bots. A typical Unity process running on a modern desktop can handle approximately 20 bots. All three Components can run within one scene graph, within one process, as in the Bots Sample Scene, or can be split between multiple machines to control hundreds of bots. Creating Bots with the Bots Scene Opening the Bots scene and pressing Play will show a birds-eye view of the local process, and a control panel. This scene is only meant to be used on the desktop. The Bots scene already has one Bot Peer created. This GameObject's NetworkScene Editor Controls or the Create Room/Join Room Control Panel Buttons can be used to immediately join the Bot into a room with other Peers - bots or normal Peers. Stress Testing with the Bots scene One of the uses for Bots is stress testing. The Bots Sample is set up to show this. The Bots Scene contains a number of Components, and a UI. Bots Control This GameObject contains resources to allow a user to control one or more Bot Managers with the UI. It also includes the Camera and Event Manager. The UI contained under Bots Control drives the BotsController , which in turn directs Bot Managers. The BotsController is in the Control Room GameObject, which has its own NetworkScene and so forms the Bots Controller Peer. This branch also contains a Peer (Bots Room Peer) that can be connected to the Bots' room, in order to do things such as take statistics or collect logs. Bots Manager The Bots Manager GameObject is the forest for the Bots Manager Peer. Though they are in the same scene graph, the Bots Manager Peer and Bots Controller Peer both join the same command and control room. Environment To reduce memory overhead, multiple Bot Peers share the same Environment, though as they have their own NetworkScene they don't directly interact outside of Ubiq networking. Bot Peer 1 The Scene includes one Bot Peer instance. The Control Panel can be used to add more. Bots Config The Bots Config GameObject hosts a helper Component to set which servers the Control Room and Bots Room should be hosted on at design time. By allowing these to be set at design time, the Bots Scene can be built to an executable to be run headless on different machines without needing further configuration. Make sure to change the Control Room Id before running the sample against Nexus, in case others are running the same sample. Controlling Bots The Bots Manager is used to spawn new Bot Peer instances. Bot Peers exist in the same application but are completley independent as far as the network, and other peers, are concerned. The UI in the Bots scene is for the Bot Controller. There is no interface for interacting with the virtual world, VR or otherwise, as there is no Network Scene for players in the Bots example. Bots can be instructed to join any room however, including those with regular players. Command and Control and Bots Rooms When the Scene is started, the Control Panel UI will control the local Bot Manager. However, there is a limit to how many Bot Peers a single Unity process can host. Bot Managers across different Unity processes can work together to control very large numbers of bots. The Control Panel and Bot Manager are two distinct Ubiq Peers. They communicate using Ubiq Messages through a 'Command and Control' Room. When the Control Panel starts, it creates a new Room and has the local Bot Manager join it. Additional Bot Managers from other processes can join this room too, and fall under the control of the Control Panel. The Command and Control Room can be any Ubiq Room, including the one that the Bots join. The Room can be set via the command line with the -commandroomjoincode argument. This, combined with -batchmode, can allow many headless Unity instances to spawn bots. In practice though, when doing things like stress testing, the Room should be different, even on a different server if doing server stress testing. Servers There are in total four distinct Peers in the single Bots Sample: the Bot Controller (Control Room), the Bot Manager (Control Room), the Bot Room Peer (Bots Room) and the default Bot Peer (Bots Room). The Bots Config Component is used to change the server(s) for all of these at once. The default server for Bot Peer 1 is set in the standard NetworkScene Prefab; it is overridden before connecting when the Control Panel is used to control the Bots. Control Panel The UI has two sets of controls: Common controls and Instance controls. Common Controls apply to all Bot Managers, and Instance controls apply to just that Bot Manager. When only one Bot Manager instance is known, the controls behave identically. Toggle Camera As each Peer acts as if it were the only one in the process, each Peer will show all other Peers Avatars. This can create high rendering loads. Avatars can be hidden by toggling the Camera, which changes the Culling Mask. The camera is not completley disabled, as it is needed for the UI. Enable Audio Enables or Disables the audio chat channel for new Bots. This can be used to reduce compute load on the clients. This may be desireable if stress testing a server, for example, as audio data doesn't pass through the server. Create/Join Room The Create Room button creates a new room for Bots , and commands any existing Bots to join it. Alternatively, an existing Join Code can be entered and the Join Room button used. Either can be used as many times as desired to change all the Bots at once, without re-creating the Bots. Number of Peers Shows the number of Peers in the Bots Room, including the 'dummy' peer. A process may host a number of Bots that have not yet joined a room. Use this figure to monitor the actual number of bots in a room together. Bot Manager Instances Below the Common Controls each Bot Manager that the Controller is aware of is listed. Each line shows the Id of the Manager, as well as a the number of bots it is hosting. The Input Field and Add Bot Button can be used add new Bots to that instance. If a Bots Room has been set up, new Bots will automatically join it. The FPS is used to approximate the performance or load of the Bots Manager. The Colour is controlled by the Fps Gradient member of the Bots Manager Control Prefab (between 0-100 Fps).","title":"BotsManager & BotsController"},{"location":"botssample/#bot-scene","text":"The Bots Application Sample also contains a sample scene with a single Bot Peer, along with a Player Peer. This can be used to see what it is like for players to interact with Bots. In this scene, the Player and the Bot should be joined to the same room manually using the RoomClient Editor controls.","title":"Bot Scene"},{"location":"creatinganetworkedobject/","text":"Building a Basic Networked Object Networked objects are Components that can keep themselves synchronised by exchanging messages over the network. You can create new Networked Objects to implement your own networked behaviour. 1) Create a new Unity Script and add it to the GameObject that you want to be networked. You can do this via the inspector by clicking on \"Add Component\" and typing the new name. 2) Include Ubiq.Messaging using System.Collections; using System.Collections.Generic; using UnityEngine; using Ubiq.Messaging; public class MyNetworkedObject : MonoBehaviour { // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } } 3) Create a new member, context . context will hold the address of your object on the network, and allow you to send messages. using System.Collections; using System.Collections.Generic; using UnityEngine; using Ubiq.Messaging; public class MyNetworkedObject : MonoBehaviour { NetworkContext context; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } } 4) Declare a method called ProcessMessage , which takes a ReferenceCountedSceneGraphMessage . This is where messages to your Component will come in. using System.Collections; using System.Collections.Generic; using UnityEngine; using Ubiq.Messaging; public class MyNetworkedObject : MonoBehaviour { NetworkContext context; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } public void ProcessMessage(ReferenceCountedSceneGraphMessage message) { } } 5) In your Start() method, call NetworkScene.Register() . This registers your Component with Ubiq and gets it an address on the network. The return value is a NetworkContext which you can store in the member created previously. using System.Collections; using System.Collections.Generic; using UnityEngine; using Ubiq.Messaging; public class MyNetworkedObject : MonoBehaviour { NetworkContext context; // Start is called before the first frame update void Start() { context = NetworkScene.Register(this); } // Update is called once per frame void Update() { } public void ProcessMessage(ReferenceCountedSceneGraphMessage message) { } } 6) Define what a message between instances of your Component will look like. In the message, write the variables that you want to send. Below we create a message to send the object's position. using System.Collections; using System.Collections.Generic; using UnityEngine; using Ubiq.Messaging; public class MyNetworkedObject : MonoBehaviour { NetworkContext context; // Start is called before the first frame update void Start() { context = NetworkScene.Register(this); } // Update is called once per frame void Update() { } private struct Message { public Vector3 position; } public void ProcessMessage(ReferenceCountedSceneGraphMessage message) { } } 7) Add code to parse and process incoming messages to ProcessMessage . Below, we convert the ReferenceCountedSceneGraphMessage into a Message, and then access the position member to set the object's position in world space. using System.Collections; using System.Collections.Generic; using UnityEngine; using Ubiq.Messaging; public class MyNetworkedObject : MonoBehaviour { NetworkContext context; // Start is called before the first frame update void Start() { context = NetworkScene.Register(this); } // Update is called once per frame void Update() { } private struct Message { public Vector3 position; } public void ProcessMessage(ReferenceCountedSceneGraphMessage message) { // Parse the message var m = message.FromJson(); // Use the message to update the Component transform.localPosition = m.position; } } 8) Messages will only be sent to your Component, from other instances of your Component, so you also need to Send messages as well. This is done through the NetworkContext you recieved when the Component was registered. Below, we check if the position of the object has changed in the last frame, and if so, send the new position to all other instances of the object. We detect if the position has changed by keeping track of the position in the last frame in a new member, lastPosition . We also modify ProcessMessage slightly, to update lastPosition when a message is received - otherwise, an incoming message will generate an outgoing message, and two Components will send messages back and forth in an endless cycle even if the player hasn't changed the objects position! using System.Collections; using System.Collections.Generic; using UnityEngine; using Ubiq.Messaging; public class MyNetworkedObject : MonoBehaviour { NetworkContext context; // Start is called before the first frame update void Start() { context = NetworkScene.Register(this); } Vector3 lastPosition; // Update is called once per frame void Update() { if(lastPosition != transform.localPosition) { lastPosition = transform.localPosition; context.SendJson(new Message() { position = transform.localPosition }); } } private struct Message { public Vector3 position; } public void ProcessMessage(ReferenceCountedSceneGraphMessage message) { // Parse the message var m = message.FromJson(); // Use the message to update the Component transform.localPosition = m.position; // Make sure the logic in Update doesn't trigger as a result of this message lastPosition = transform.localPosition; } } 9) Your first networked object is now complete! Add a cube to your object so you can see it in the scene. Continue with the tutorials to see it in action!","title":"Creating Networked Objects"},{"location":"creatinganetworkedobject/#building-a-basic-networked-object","text":"Networked objects are Components that can keep themselves synchronised by exchanging messages over the network. You can create new Networked Objects to implement your own networked behaviour. 1) Create a new Unity Script and add it to the GameObject that you want to be networked. You can do this via the inspector by clicking on \"Add Component\" and typing the new name. 2) Include Ubiq.Messaging using System.Collections; using System.Collections.Generic; using UnityEngine; using Ubiq.Messaging; public class MyNetworkedObject : MonoBehaviour { // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } } 3) Create a new member, context . context will hold the address of your object on the network, and allow you to send messages. using System.Collections; using System.Collections.Generic; using UnityEngine; using Ubiq.Messaging; public class MyNetworkedObject : MonoBehaviour { NetworkContext context; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } } 4) Declare a method called ProcessMessage , which takes a ReferenceCountedSceneGraphMessage . This is where messages to your Component will come in. using System.Collections; using System.Collections.Generic; using UnityEngine; using Ubiq.Messaging; public class MyNetworkedObject : MonoBehaviour { NetworkContext context; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { } public void ProcessMessage(ReferenceCountedSceneGraphMessage message) { } } 5) In your Start() method, call NetworkScene.Register() . This registers your Component with Ubiq and gets it an address on the network. The return value is a NetworkContext which you can store in the member created previously. using System.Collections; using System.Collections.Generic; using UnityEngine; using Ubiq.Messaging; public class MyNetworkedObject : MonoBehaviour { NetworkContext context; // Start is called before the first frame update void Start() { context = NetworkScene.Register(this); } // Update is called once per frame void Update() { } public void ProcessMessage(ReferenceCountedSceneGraphMessage message) { } } 6) Define what a message between instances of your Component will look like. In the message, write the variables that you want to send. Below we create a message to send the object's position. using System.Collections; using System.Collections.Generic; using UnityEngine; using Ubiq.Messaging; public class MyNetworkedObject : MonoBehaviour { NetworkContext context; // Start is called before the first frame update void Start() { context = NetworkScene.Register(this); } // Update is called once per frame void Update() { } private struct Message { public Vector3 position; } public void ProcessMessage(ReferenceCountedSceneGraphMessage message) { } } 7) Add code to parse and process incoming messages to ProcessMessage . Below, we convert the ReferenceCountedSceneGraphMessage into a Message, and then access the position member to set the object's position in world space. using System.Collections; using System.Collections.Generic; using UnityEngine; using Ubiq.Messaging; public class MyNetworkedObject : MonoBehaviour { NetworkContext context; // Start is called before the first frame update void Start() { context = NetworkScene.Register(this); } // Update is called once per frame void Update() { } private struct Message { public Vector3 position; } public void ProcessMessage(ReferenceCountedSceneGraphMessage message) { // Parse the message var m = message.FromJson(); // Use the message to update the Component transform.localPosition = m.position; } } 8) Messages will only be sent to your Component, from other instances of your Component, so you also need to Send messages as well. This is done through the NetworkContext you recieved when the Component was registered. Below, we check if the position of the object has changed in the last frame, and if so, send the new position to all other instances of the object. We detect if the position has changed by keeping track of the position in the last frame in a new member, lastPosition . We also modify ProcessMessage slightly, to update lastPosition when a message is received - otherwise, an incoming message will generate an outgoing message, and two Components will send messages back and forth in an endless cycle even if the player hasn't changed the objects position! using System.Collections; using System.Collections.Generic; using UnityEngine; using Ubiq.Messaging; public class MyNetworkedObject : MonoBehaviour { NetworkContext context; // Start is called before the first frame update void Start() { context = NetworkScene.Register(this); } Vector3 lastPosition; // Update is called once per frame void Update() { if(lastPosition != transform.localPosition) { lastPosition = transform.localPosition; context.SendJson(new Message() { position = transform.localPosition }); } } private struct Message { public Vector3 position; } public void ProcessMessage(ReferenceCountedSceneGraphMessage message) { // Parse the message var m = message.FromJson(); // Use the message to update the Component transform.localPosition = m.position; // Make sure the logic in Update doesn't trigger as a result of this message lastPosition = transform.localPosition; } } 9) Your first networked object is now complete! Add a cube to your object so you can see it in the scene. Continue with the tutorials to see it in action!","title":"Building a Basic Networked Object"},{"location":"eventloganalysis/","text":"Analysis The Event Logger outputs structured logs, as Json objects. These can be processed on any platform that can read Json files. A sample log file is shown below. [ {\"ticks\":637799309335620180,\"peer\":\"088edbc1-1d1f09b5\",\"type\":\"Ubiq.Messaging.NetworkScene\",\"event\":\"Awake\",\"arg1\":\"DESKTOP-F1J0MRR\",\"arg2\":\"System Product Name (ASUS)\",\"arg3\":\"f73fe01b1e21031d49274a1491d1d6b5714c92e9\"}, {\"ticks\":637799309384207356,\"peer\":\"088edbc1-1d1f09b5\",\"type\":\"Ubiq.Voip.VoipPeerConnectionManager\",\"objectid\":\"088edbc1-1d1f09b5\",\"componentid\":50,\"event\":\"CreatePeerConnectionForPeer\",\"arg1\":\"6c494697-2e79f5e3\",\"arg2\":\"26a6ee77-3cec71fe\"}, {\"ticks\":637799309384277353,\"peer\":\"088edbc1-1d1f09b5\",\"type\":\"Ubiq.Voip.VoipPeerConnectionManager\",\"objectid\":\"088edbc1-1d1f09b5\",\"componentid\":50,\"event\":\"RequestPeerConnection\",\"arg1\":\"6c494697-2e79f5e3\",\"arg2\":\"26a6ee77-3cec71fe\"}, {\"ticks\":637799309087959820,\"peer\":\"26a6ee77-3cec71fe\",\"type\":\"Ubiq.Messaging.NetworkScene\",\"event\":\"Awake\",\"arg1\":\"Oculus Quest\",\"arg2\":\"Oculus Quest\",\"arg3\":\"b8db4746286db62ecad4c6fa13f17ab6\"}, {\"ticks\":637799309303272560,\"peer\":\"26a6ee77-3cec71fe\",\"type\":\"Ubiq.Voip.VoipPeerConnectionManager\",\"objectid\":\"26a6ee77-3cec71fe\",\"componentid\":50,\"event\":\"CreatePeerConnectionForRequest\",\"arg1\":\"6c494697-2e79f5e3\"} ] In this example, two peers - a desktop PC (Unity Editor) and an Oculus Quest - join a room. The NetworkScene and VoipPeerConnectionManager both log events. Some Json members are defined by the Emitter type. For example, the ContextLogger writes the objectid of the context passed to it on creation. The arg members correspond to those passed to the Log() method. All entries include a timestamp and the Id of the Peer that generated the log. Timestamps are given in .Net Ticks . Python Python can be used to analys logs programmatically. The Jupyter notebook below shows how to import and process logs using Pandas , a powerful data analysis library for Python. See the Notebook in full Download Jupyter Notebook Download Example Log File Excel Structured event logs are amenable to being viewed in a table. Microsoft Excel PowerQuery can import Json files and load events into Excel Worksheets. To do this: Open a new Workbook From the Data tab, choose Get Data -> From File -> From Json Open the log file, for example Application_log_2021-04-23-10-56-03_0.json Select the List header and click Convert To Table . This will instruct Excel to treat each entry as a row. Leave the Default Values in place and Click OK . The View will now appear as a Column . Use the button in the top right to add the Expand Column step. This will split each record into a set of columns. Make sure to click Load More... if visible to ensure you get every possible field in the table. Click OK Click Close & Load to build your table. You can now order by Ticks, and filter columns such as Events. Matlab Like Python, Matlab can load Json using the jsondecode function. % Read the text file and use jsondecode to produce a cell array of % structures. events = jsondecode(fileread(\"Debug_Log.json\")); % The structures will have different fields, so we must use loops to filter % them before they can be combined into a single struct array or table. % Below, find all the events of type SpawnObject, and combine them into a % new array. spawn = []; for i = 1:numel(events) % The curly braces access the contents of the cell i, which is the % struct itself. s = events{i}; if categorical(cellstr(s.event)) == categorical(\"SpawnObject\") spawn = [spawn; s]; end end % Convert the new array into a table T = struct2table(spawn); % Use the table to change the type of the sceneid column so we can easily % split the events by which peer they are from. T.peer = categorical(T.peer); % Filter the events to keep only those emitted by the Peer that initiated % the spawn T = T(T.arg3,:); % Plot the number of objects spawned over time, by each Peer figure; hold all; peers = unique(T.peer); for p = peers' spawned = T(T.peer == p,:); plot(spawned.ticks,1:size(spawned,1)); end xlabel(\"Time (ticks)\"); ylabel(\"Number of Objects\"); legend(peers); Download Matlab Source Download Example Log File","title":"Analysis"},{"location":"eventloganalysis/#analysis","text":"The Event Logger outputs structured logs, as Json objects. These can be processed on any platform that can read Json files. A sample log file is shown below. [ {\"ticks\":637799309335620180,\"peer\":\"088edbc1-1d1f09b5\",\"type\":\"Ubiq.Messaging.NetworkScene\",\"event\":\"Awake\",\"arg1\":\"DESKTOP-F1J0MRR\",\"arg2\":\"System Product Name (ASUS)\",\"arg3\":\"f73fe01b1e21031d49274a1491d1d6b5714c92e9\"}, {\"ticks\":637799309384207356,\"peer\":\"088edbc1-1d1f09b5\",\"type\":\"Ubiq.Voip.VoipPeerConnectionManager\",\"objectid\":\"088edbc1-1d1f09b5\",\"componentid\":50,\"event\":\"CreatePeerConnectionForPeer\",\"arg1\":\"6c494697-2e79f5e3\",\"arg2\":\"26a6ee77-3cec71fe\"}, {\"ticks\":637799309384277353,\"peer\":\"088edbc1-1d1f09b5\",\"type\":\"Ubiq.Voip.VoipPeerConnectionManager\",\"objectid\":\"088edbc1-1d1f09b5\",\"componentid\":50,\"event\":\"RequestPeerConnection\",\"arg1\":\"6c494697-2e79f5e3\",\"arg2\":\"26a6ee77-3cec71fe\"}, {\"ticks\":637799309087959820,\"peer\":\"26a6ee77-3cec71fe\",\"type\":\"Ubiq.Messaging.NetworkScene\",\"event\":\"Awake\",\"arg1\":\"Oculus Quest\",\"arg2\":\"Oculus Quest\",\"arg3\":\"b8db4746286db62ecad4c6fa13f17ab6\"}, {\"ticks\":637799309303272560,\"peer\":\"26a6ee77-3cec71fe\",\"type\":\"Ubiq.Voip.VoipPeerConnectionManager\",\"objectid\":\"26a6ee77-3cec71fe\",\"componentid\":50,\"event\":\"CreatePeerConnectionForRequest\",\"arg1\":\"6c494697-2e79f5e3\"} ] In this example, two peers - a desktop PC (Unity Editor) and an Oculus Quest - join a room. The NetworkScene and VoipPeerConnectionManager both log events. Some Json members are defined by the Emitter type. For example, the ContextLogger writes the objectid of the context passed to it on creation. The arg members correspond to those passed to the Log() method. All entries include a timestamp and the Id of the Peer that generated the log. Timestamps are given in .Net Ticks .","title":"Analysis"},{"location":"eventloganalysis/#python","text":"Python can be used to analys logs programmatically. The Jupyter notebook below shows how to import and process logs using Pandas , a powerful data analysis library for Python. See the Notebook in full Download Jupyter Notebook Download Example Log File","title":"Python"},{"location":"eventloganalysis/#excel","text":"Structured event logs are amenable to being viewed in a table. Microsoft Excel PowerQuery can import Json files and load events into Excel Worksheets. To do this: Open a new Workbook From the Data tab, choose Get Data -> From File -> From Json Open the log file, for example Application_log_2021-04-23-10-56-03_0.json Select the List header and click Convert To Table . This will instruct Excel to treat each entry as a row. Leave the Default Values in place and Click OK . The View will now appear as a Column . Use the button in the top right to add the Expand Column step. This will split each record into a set of columns. Make sure to click Load More... if visible to ensure you get every possible field in the table. Click OK Click Close & Load to build your table. You can now order by Ticks, and filter columns such as Events.","title":"Excel"},{"location":"eventloganalysis/#matlab","text":"Like Python, Matlab can load Json using the jsondecode function. % Read the text file and use jsondecode to produce a cell array of % structures. events = jsondecode(fileread(\"Debug_Log.json\")); % The structures will have different fields, so we must use loops to filter % them before they can be combined into a single struct array or table. % Below, find all the events of type SpawnObject, and combine them into a % new array. spawn = []; for i = 1:numel(events) % The curly braces access the contents of the cell i, which is the % struct itself. s = events{i}; if categorical(cellstr(s.event)) == categorical(\"SpawnObject\") spawn = [spawn; s]; end end % Convert the new array into a table T = struct2table(spawn); % Use the table to change the type of the sceneid column so we can easily % split the events by which peer they are from. T.peer = categorical(T.peer); % Filter the events to keep only those emitted by the Peer that initiated % the spawn T = T(T.arg3,:); % Plot the number of objects spawned over time, by each Peer figure; hold all; peers = unique(T.peer); for p = peers' spawned = T(T.peer == p,:); plot(spawned.ticks,1:size(spawned,1)); end xlabel(\"Time (ticks)\"); ylabel(\"Number of Objects\"); legend(peers); Download Matlab Source Download Example Log File","title":"Matlab"},{"location":"eventlogging/","text":"Introduction Networked VR applications require different types of logging, such as: Debug Logs Experiment Logs Network Traces Refers to logging expected and exceptional events that occur during a regular session. The purpose is post-hoc debugging of high-level application code. Refers to logging application-specific data, such as measurements or questionnaire responses for an experiment. Refers to captures of network traffic to investigate reproducible low-level netcode bugs. (1) & (2) are handled by Ubiq's Event Logging system. (3) has distinct performance implications, so is handled seperately. Use Case The Event Logging System is for collecting low or medium frequency events from multiple peers. The Event Logging system can log both Ubiq and third-party events, which can then be extracted and analysed. Events are discrete, but otherwise have very few restrictions. It is up to the user to ensure that event logging in their application doesn't negatively affect performance. Overview Events are generated by LogEmitter instances placed throughout the application. Events generated by these components are passed to a local LogCollector instance. The LogCollector then writes them to disk (or database, or other endpoint), or forwards them to another LogCollector that will. LogEmitter LogEmitter instances are lightweight objects that the application uses to log events. Calls to a LogEmitter are expected to be placed throughout the system persistently, rather than gated with pre-processor defines. The most common types of event logger are the ContextEventLogger , which is designed to work with Components that have a NetworkContext , and the ExperimentEventLogger , designed for logging measurements in experiments. public class VoipPeerConnectionManager : MonoBehaviour { private ContextLogEmitter debug; private void Start() { context = NetworkScene.Register(this); debug = new ContextLogEmitter(context); } public void ProcessMessage(ReferenceCountedSceneGraphMessage message) { var msg = JsonUtility.FromJson(message.ToString()); switch (msg.type) { case \"RequestPeerConnection\": debug.Log(\"CreatePeerConnectionForRequest\", msg.objectid); break; } } } The snippet above demonstrates the creation and use of a ContextEventLogger . The VoipPeerConnectionManager declares the ContextLogEmitter debug and initialises it with a ContextEventLogger once a context has been created. The Log method can then be called to log the receipt of a specific message. Events are recorded as Json objects, for example: { \"ticks\":637794978937526996, \"peer\":\"aa9a2c8c-0c6f11c7\", \"type\":\"Ubiq.Voip.VoipPeerConnectionManager\", \"objectid\":\"aa9a2c8c-0c6f11c7\", \"componentid\":50, \"event\":\"CreatePeerConnectionForRequest\", \"arg1\":\"9020d814-45c41c19\" } LogEmitter instances attach to a single LogCollector . The emitter constructors find the closest LogCollector automatically. LogEmitter methods can be safely called from outside the Unity main thread. They should not be called from outside CLR threads however. LogEmitter instances are designed to have zero overhead when logs are not actually written. The Log method has many overloads to avoid boxing, and serialisation only runs when logging is on. Logs are only written when there is a listening LogCollector in the scene. If the LogCollector is disabled, non-existent, or the Collector's EventFilter ignores the emitter's event type, the emitter will not do anything. It is encouraged to make as many LogEmitter instances as needed. Individual emitters are simple, with few options. Use multiple LogEmitter instances within a class to get fine-grained control over logging, for example different log levels. Events and Filters A LogEmitter can tag events with a flag. A number of flags are pre-defined in Ubiq.Logging.EventType enum. The underlying type is an sbyte , so additional values can be used in an unchecked context. [Flags] enum EventType { Application = 1, Experiment = 2, Debug = 4, Info = 8 } A LogCollector can be set to ignore one or more of these types with its EventsFilter member. If a flag is set the event is logged. If it is unset the event is not logged. Filtering applies to local events. LogCollectors will forward and write all external events regardless of the filter. Filtered events are not cached, but lost permanently. LogCollector writes events of different types to different streams/files. Prefixes are defined for the entries in Ubiq.Logging.EventType . Other tags will display as their numerical equivalent. The LogCollector EventsFilter member and the Tag member of any LogEmitter can be changed at any time. ComponentEventLogger and ContextEventLogger have their type set to Debug by default. Convenience classes are defined for ExperimentLogEmitter and InfoLogEmitter . LogCollector LogCollector instances receive events from local LogEmitter instances and remote LogCollector instances. They do one of three things with these events: Cache them (Buffering Mode) Write them to a local file (Writing Mode) Forward them to another LogCollector (Forwarding Mode) The behaviour depends on the value of the destination member, which determines where the events should go. LogCollector instances can be organised this way into complex tree arrangements. However, the expected use case is that Peers in a Group forward events directly to one Collector, that also writes them to disk. LogCollector writes events to files in the Persistent Data folder of whatever platform it is running on. It does this through a set of Ubiq.Logging.LogCollector.IOutputStream instances (one for each event type, created on demand). The filenames include the event type and a timestamp. Buffering When a LogCollector is not Forwarding or Writing, it will cache or Buffer all events received (whether local or remote). If a LogCollector was previously Writing or Forwarding and stops, it will resume Buffering. When a LogCollector begins Writing or Forwarding, it will start with all the events in its buffer. Events only leave the buffer when being sent to a safe destination (another LogCollector , or a file). In this way logging is lossless, regardless of the initialisation order and even when changing the Active LogCollector . However, events that leave the buffer are gone permanently. If a LogCollector writes events, then another LogCollector becomes active at a later time, the second will only output events received after it became active. The file from the first LogCollector must be retrieved out-of-band to have the complete record of events. This can be avoided by ensuring only one LogCollector becomes active during the lifetime of the Peer Group. Encodings Events are written as Json (Utf8 strings). LogCollector attempts to make the output files Json compliant, by placing events in a top-level Json Array , with comma seperated entries. If the file is being read live however, or the process terminates unexpectedly, the file will not have the closing bracket ( ] ). In this case the application reading the files should either add the bracket to the end of the stream or perform its own tokenisation. Changing the LogCollector Only one LogCollector in a Peer Group may be in Writing Mode at any time. The Active Collector can be changed by calling StartCollection on the new Collector. This can be done at any time. When the Active Collector changes, the Peers must agree on the new Collector. This is a Distributed Agreement problem. LogCollector solves this using the Global Snapshot method [1]. The new Collector broadcasts a message with a logical clock value and atomically updates its cut state. When Peers receive the message, they compare the clock against their own. If it is greater, they update their cut state, otherwise the message is ignored. If a Collector is attempting to become Active, and receives a message with the same clock value as its own, a collision has occured. In this case, the Collector re-initialises its clock to a random value, and re-transmits its state. If a message is received with a greater clock value, it updates its own State as if it were any other Peer. This algorithm assumes that Peers are fully connected, and message passing is reliable, which is true in Ubiq. The logical clock ensures that all Peers converge on the same State, regardless of the order the messages were recieved in. The Peers have converged when all messages have been delivered. When the Active Collector leaves the Peer Group, the Clock is reset. Limitations As long as Peers remain connected, the system is lossless, even during convergence. However, during convergence different events may flow to the old and new Active Collectors at the same time, resulting in events being spread between the sets of log files. This will occur until convergence. There is no upper bound on this time, and there is no mechanism to check if convergence has been achieved, so StartCollection should be called with care. To surrender position as the Active Collector, a Collector may call StopCollection , however the time between this and the Peers converging to null is again indeterminate. When the Active Collector changes, the delay between the previous Collector receiving the new state, and other Peers in the Group receiving the new state, may cause it to behave as a relay for a time. Process Failure If the Active Collector process fails, log events will be lost until the system converges on a new Collector or null. If the Collector was part of a Room, the Rooms system can detect disconnection in some cases, and Collectors will update their cut state independently. The Collectors in this case will cache until a new Collector volunteers itself. If a process that was not the Active Collector fails, then that process will simply not emit events. If that process was previously an Active Collector, it is still possible to lose events if the Collector was acting as a relay when the process failed. For collecting data such as experimental logs, it is recommended to only ever have one collector on a process that can be monitored. For example, the logcollectorservice . Verification The LogCollector method GetBufferedEventCount returns the number of Events currently buffered. As LogCollector is multi-threaded, this count may change even while it is being returned. However, if it is known, for example, that an application will not generate any new Events of a particular type, it can be used to check whether all of those events have finished writing. The LogCollector method Ping will ping the Active Collector. All Log messages are delivered in order. Therefore these mechanisms can be used together to verify that all log messages for a particular application have been delivered successfully before it exits. To do this, an application could: Write the last Event, e.g. a Questionnaire result. In the same thread, wait until the number of buffered events of that type is zero. Ping the Active Collector When a response is receieved, the Event, and all preceeding it, will have been successfully delivered and the application can safely exit. This protocol is implemented in the WaitForTransmitComplete method. This does not protect against process failure of an intermediary LogCollector however, a condition which is irrecoverable. Reliability In order for WaitForTransmitComplete to confirm that an event has been successfully delivered, the integrity of the LogCollector processes must be fully visible. One way to achieve this is to ensure only one LogCollector is ever active, and that that LogCollector never forwards. In this case, if the Ping is received, then the logs must also be successfully delivered as they cannot have taken any other path to the collector. LogCollector will keep track of this by default, and warn any caller of WaitForTransmitComplete if this is the case. Analysis A LogCollector outputs a stream of structured logs in compliant Json. These logs can be fed to a stack like the ELK, processed with third-party tools like Matlab or Excel, or processed programmatically on platforms such as Python. See the Analysis section for examples of how to process the logs. [1] Kshemkalyani, A. D., & Singhal, M. (2008). Distributed Computing: Principles, Algorithms and Practice. Cambridge University Press.","title":"Introduction"},{"location":"eventlogging/#introduction","text":"Networked VR applications require different types of logging, such as: Debug Logs Experiment Logs Network Traces Refers to logging expected and exceptional events that occur during a regular session. The purpose is post-hoc debugging of high-level application code. Refers to logging application-specific data, such as measurements or questionnaire responses for an experiment. Refers to captures of network traffic to investigate reproducible low-level netcode bugs. (1) & (2) are handled by Ubiq's Event Logging system. (3) has distinct performance implications, so is handled seperately.","title":"Introduction"},{"location":"eventlogging/#use-case","text":"The Event Logging System is for collecting low or medium frequency events from multiple peers. The Event Logging system can log both Ubiq and third-party events, which can then be extracted and analysed. Events are discrete, but otherwise have very few restrictions. It is up to the user to ensure that event logging in their application doesn't negatively affect performance.","title":"Use Case"},{"location":"eventlogging/#overview","text":"Events are generated by LogEmitter instances placed throughout the application. Events generated by these components are passed to a local LogCollector instance. The LogCollector then writes them to disk (or database, or other endpoint), or forwards them to another LogCollector that will.","title":"Overview"},{"location":"eventlogging/#logemitter","text":"LogEmitter instances are lightweight objects that the application uses to log events. Calls to a LogEmitter are expected to be placed throughout the system persistently, rather than gated with pre-processor defines. The most common types of event logger are the ContextEventLogger , which is designed to work with Components that have a NetworkContext , and the ExperimentEventLogger , designed for logging measurements in experiments. public class VoipPeerConnectionManager : MonoBehaviour { private ContextLogEmitter debug; private void Start() { context = NetworkScene.Register(this); debug = new ContextLogEmitter(context); } public void ProcessMessage(ReferenceCountedSceneGraphMessage message) { var msg = JsonUtility.FromJson(message.ToString()); switch (msg.type) { case \"RequestPeerConnection\": debug.Log(\"CreatePeerConnectionForRequest\", msg.objectid); break; } } } The snippet above demonstrates the creation and use of a ContextEventLogger . The VoipPeerConnectionManager declares the ContextLogEmitter debug and initialises it with a ContextEventLogger once a context has been created. The Log method can then be called to log the receipt of a specific message. Events are recorded as Json objects, for example: { \"ticks\":637794978937526996, \"peer\":\"aa9a2c8c-0c6f11c7\", \"type\":\"Ubiq.Voip.VoipPeerConnectionManager\", \"objectid\":\"aa9a2c8c-0c6f11c7\", \"componentid\":50, \"event\":\"CreatePeerConnectionForRequest\", \"arg1\":\"9020d814-45c41c19\" } LogEmitter instances attach to a single LogCollector . The emitter constructors find the closest LogCollector automatically. LogEmitter methods can be safely called from outside the Unity main thread. They should not be called from outside CLR threads however. LogEmitter instances are designed to have zero overhead when logs are not actually written. The Log method has many overloads to avoid boxing, and serialisation only runs when logging is on. Logs are only written when there is a listening LogCollector in the scene. If the LogCollector is disabled, non-existent, or the Collector's EventFilter ignores the emitter's event type, the emitter will not do anything. It is encouraged to make as many LogEmitter instances as needed. Individual emitters are simple, with few options. Use multiple LogEmitter instances within a class to get fine-grained control over logging, for example different log levels.","title":"LogEmitter"},{"location":"eventlogging/#events-and-filters","text":"A LogEmitter can tag events with a flag. A number of flags are pre-defined in Ubiq.Logging.EventType enum. The underlying type is an sbyte , so additional values can be used in an unchecked context. [Flags] enum EventType { Application = 1, Experiment = 2, Debug = 4, Info = 8 } A LogCollector can be set to ignore one or more of these types with its EventsFilter member. If a flag is set the event is logged. If it is unset the event is not logged. Filtering applies to local events. LogCollectors will forward and write all external events regardless of the filter. Filtered events are not cached, but lost permanently. LogCollector writes events of different types to different streams/files. Prefixes are defined for the entries in Ubiq.Logging.EventType . Other tags will display as their numerical equivalent. The LogCollector EventsFilter member and the Tag member of any LogEmitter can be changed at any time. ComponentEventLogger and ContextEventLogger have their type set to Debug by default. Convenience classes are defined for ExperimentLogEmitter and InfoLogEmitter .","title":"Events and Filters"},{"location":"eventlogging/#logcollector","text":"LogCollector instances receive events from local LogEmitter instances and remote LogCollector instances. They do one of three things with these events: Cache them (Buffering Mode) Write them to a local file (Writing Mode) Forward them to another LogCollector (Forwarding Mode) The behaviour depends on the value of the destination member, which determines where the events should go. LogCollector instances can be organised this way into complex tree arrangements. However, the expected use case is that Peers in a Group forward events directly to one Collector, that also writes them to disk. LogCollector writes events to files in the Persistent Data folder of whatever platform it is running on. It does this through a set of Ubiq.Logging.LogCollector.IOutputStream instances (one for each event type, created on demand). The filenames include the event type and a timestamp.","title":"LogCollector"},{"location":"eventlogging/#buffering","text":"When a LogCollector is not Forwarding or Writing, it will cache or Buffer all events received (whether local or remote). If a LogCollector was previously Writing or Forwarding and stops, it will resume Buffering. When a LogCollector begins Writing or Forwarding, it will start with all the events in its buffer. Events only leave the buffer when being sent to a safe destination (another LogCollector , or a file). In this way logging is lossless, regardless of the initialisation order and even when changing the Active LogCollector . However, events that leave the buffer are gone permanently. If a LogCollector writes events, then another LogCollector becomes active at a later time, the second will only output events received after it became active. The file from the first LogCollector must be retrieved out-of-band to have the complete record of events. This can be avoided by ensuring only one LogCollector becomes active during the lifetime of the Peer Group.","title":"Buffering"},{"location":"eventlogging/#encodings","text":"Events are written as Json (Utf8 strings). LogCollector attempts to make the output files Json compliant, by placing events in a top-level Json Array , with comma seperated entries. If the file is being read live however, or the process terminates unexpectedly, the file will not have the closing bracket ( ] ). In this case the application reading the files should either add the bracket to the end of the stream or perform its own tokenisation.","title":"Encodings"},{"location":"eventlogging/#changing-the-logcollector","text":"Only one LogCollector in a Peer Group may be in Writing Mode at any time. The Active Collector can be changed by calling StartCollection on the new Collector. This can be done at any time. When the Active Collector changes, the Peers must agree on the new Collector. This is a Distributed Agreement problem. LogCollector solves this using the Global Snapshot method [1]. The new Collector broadcasts a message with a logical clock value and atomically updates its cut state. When Peers receive the message, they compare the clock against their own. If it is greater, they update their cut state, otherwise the message is ignored. If a Collector is attempting to become Active, and receives a message with the same clock value as its own, a collision has occured. In this case, the Collector re-initialises its clock to a random value, and re-transmits its state. If a message is received with a greater clock value, it updates its own State as if it were any other Peer. This algorithm assumes that Peers are fully connected, and message passing is reliable, which is true in Ubiq. The logical clock ensures that all Peers converge on the same State, regardless of the order the messages were recieved in. The Peers have converged when all messages have been delivered. When the Active Collector leaves the Peer Group, the Clock is reset.","title":"Changing the LogCollector"},{"location":"eventlogging/#limitations","text":"As long as Peers remain connected, the system is lossless, even during convergence. However, during convergence different events may flow to the old and new Active Collectors at the same time, resulting in events being spread between the sets of log files. This will occur until convergence. There is no upper bound on this time, and there is no mechanism to check if convergence has been achieved, so StartCollection should be called with care. To surrender position as the Active Collector, a Collector may call StopCollection , however the time between this and the Peers converging to null is again indeterminate. When the Active Collector changes, the delay between the previous Collector receiving the new state, and other Peers in the Group receiving the new state, may cause it to behave as a relay for a time.","title":"Limitations"},{"location":"eventlogging/#process-failure","text":"If the Active Collector process fails, log events will be lost until the system converges on a new Collector or null. If the Collector was part of a Room, the Rooms system can detect disconnection in some cases, and Collectors will update their cut state independently. The Collectors in this case will cache until a new Collector volunteers itself. If a process that was not the Active Collector fails, then that process will simply not emit events. If that process was previously an Active Collector, it is still possible to lose events if the Collector was acting as a relay when the process failed. For collecting data such as experimental logs, it is recommended to only ever have one collector on a process that can be monitored. For example, the logcollectorservice .","title":"Process Failure"},{"location":"eventlogging/#verification","text":"The LogCollector method GetBufferedEventCount returns the number of Events currently buffered. As LogCollector is multi-threaded, this count may change even while it is being returned. However, if it is known, for example, that an application will not generate any new Events of a particular type, it can be used to check whether all of those events have finished writing. The LogCollector method Ping will ping the Active Collector. All Log messages are delivered in order. Therefore these mechanisms can be used together to verify that all log messages for a particular application have been delivered successfully before it exits. To do this, an application could: Write the last Event, e.g. a Questionnaire result. In the same thread, wait until the number of buffered events of that type is zero. Ping the Active Collector When a response is receieved, the Event, and all preceeding it, will have been successfully delivered and the application can safely exit. This protocol is implemented in the WaitForTransmitComplete method. This does not protect against process failure of an intermediary LogCollector however, a condition which is irrecoverable.","title":"Verification"},{"location":"eventlogging/#reliability","text":"In order for WaitForTransmitComplete to confirm that an event has been successfully delivered, the integrity of the LogCollector processes must be fully visible. One way to achieve this is to ensure only one LogCollector is ever active, and that that LogCollector never forwards. In this case, if the Ping is received, then the logs must also be successfully delivered as they cannot have taken any other path to the collector. LogCollector will keep track of this by default, and warn any caller of WaitForTransmitComplete if this is the case.","title":"Reliability"},{"location":"eventlogging/#analysis","text":"A LogCollector outputs a stream of structured logs in compliant Json. These logs can be fed to a stack like the ELK, processed with third-party tools like Matlab or Excel, or processed programmatically on platforms such as Python. See the Analysis section for examples of how to process the logs. [1] Kshemkalyani, A. D., & Singhal, M. (2008). Distributed Computing: Principles, Algorithms and Practice. Cambridge University Press.","title":"Analysis"},{"location":"eventloggingcollectorservice/","text":"Log Collector Service The LogCollectorService is an example NodeJs application that joins a room and writes all the Experiment Log Events (0x2) in that Room to disk. The sample is located in the Node/samples/logcollectorservice directory. Use Case The Log Collector Service can be used to automatically collect data from self-directed experiments. To do this, an experimentor would create a build that automatically joined a pre-defined Room. Additionally, the Log Collector Service would be started on a server and configured to join the same Room. As participants ran their builds and completed the experiment, they would all join the same room and forward their events to the LogCollector running in the Log Collector Service, which would write those events to disk on the server. Questionnaire Scene A good way to try the Log Collector Service is to use the Questionnaire Sample (Samples/Single/Questionnaire). Add the Join Room Component to the NetworkScene, and set the Room GUID to the same one as in the logcollectorservice app. Be sure to generate a new GUID to avoid collisions with others potentially trying the same demonstration. Then, start and stop the Questionnaire scene, submitting the Questionnaire a few times each run. In the logcollectorservice directory, a number of log files should be created, with the Ids that the Peer took on each time it started. Configuration The logcollectorservice application is configured by changing the source code of app.js. The two variables that are likely to change are the log event type, and the room GUID.","title":"LogCollector Service"},{"location":"eventloggingcollectorservice/#log-collector-service","text":"The LogCollectorService is an example NodeJs application that joins a room and writes all the Experiment Log Events (0x2) in that Room to disk. The sample is located in the Node/samples/logcollectorservice directory.","title":"Log Collector Service"},{"location":"eventloggingcollectorservice/#use-case","text":"The Log Collector Service can be used to automatically collect data from self-directed experiments. To do this, an experimentor would create a build that automatically joined a pre-defined Room. Additionally, the Log Collector Service would be started on a server and configured to join the same Room. As participants ran their builds and completed the experiment, they would all join the same room and forward their events to the LogCollector running in the Log Collector Service, which would write those events to disk on the server.","title":"Use Case"},{"location":"eventloggingcollectorservice/#questionnaire-scene","text":"A good way to try the Log Collector Service is to use the Questionnaire Sample (Samples/Single/Questionnaire). Add the Join Room Component to the NetworkScene, and set the Room GUID to the same one as in the logcollectorservice app. Be sure to generate a new GUID to avoid collisions with others potentially trying the same demonstration. Then, start and stop the Questionnaire scene, submitting the Questionnaire a few times each run. In the logcollectorservice directory, a number of log files should be created, with the Ids that the Peer took on each time it started.","title":"Questionnaire Scene"},{"location":"eventloggingcollectorservice/#configuration","text":"The logcollectorservice application is configured by changing the source code of app.js. The two variables that are likely to change are the log event type, and the room GUID.","title":"Configuration"},{"location":"eventloggingexperimentquestionnaire/","text":"Questionnaire The Questionnaire Sample (Samples/Single/Questionnaire) shows how the Event Logging System may be used to collect questionnaire responses. This scene contains a panel with an example Component, Questionnaire attached to it. The Component iterates over all Slider instances under its GameObject , and uses an ExperimentLogEmitter to write their values when the user clicks Done . public class Questionnaire : MonoBehaviour { LogEmitter results; // Start is called before the first frame update void Start() { results = new ExperimentLogEmitter(this); } public void Done() { foreach (var item in GetComponentsInChildren()) { results.Log(\"Answer\", item.name, item.value); } } } The Questionnaire can be completed locally in Play Mode. Alternatively, the scene can be run remotely, and the experimentor in the Editor can join the same room as the remote copy. In either case, the experimentor in the Editor can click Start Collection on the NetworkScene > Log Manager > LogCollector to recieve the Questionnaire results. The experimentor can click Start Collection before or after the questionnaire has been completed, and the participant can complete the Questionnaire before or after joining the room. In all cases the results will be receieved correctly. Sample Output Below is the resulting Experiment log file from an application built with the Questionnaire scene. [ {\"ticks\":637795003787005516,\"peer\":\"f7d98080-7c7b05ca\",\"event\":\"Answer\",\"arg1\":\"Slider 1\",\"arg2\":0.707253}, {\"ticks\":637795003787045512,\"peer\":\"f7d98080-7c7b05ca\",\"event\":\"Answer\",\"arg1\":\"Slider 2\",\"arg2\":0.30657154}, {\"ticks\":637795003787045512,\"peer\":\"f7d98080-7c7b05ca\",\"event\":\"Answer\",\"arg1\":\"Slider 3\",\"arg2\":0.7034317} ] The Questionnaire was filled in on an Oculus Quest, after joining the same room as a user running the same scene in the Unity Editor. As soon as the Questionnaire was completed, the Unity Editor user could find the Experiment log by clicking the Open Folder button of the LogCollector Component in the Editor. Since no filters were set up on the LogManager , a Debug log for the session is also created in the same folder. [ {\"ticks\":637795043778071253,\"peer\":\"cbc6f82b-24ec48b3\",\"type\":\"Ubiq.Messaging.NetworkScene\",\"event\":\"Awake\",\"arg1\":\"DESKTOP-F1J0MRR\",\"arg2\":\"System Product Name (ASUS)\",\"arg3\":\"f73fe01b1e21031d49274a1491d1d6b5714c92e9\"}, {\"ticks\":637795044161926844,\"peer\":\"cbc6f82b-24ec48b3\",\"type\":\"Ubiq.Samples.NetworkSpawner\",\"objectid\":\"7725a971-a3692643\",\"componentid\":49018,\"event\":\"SpawnObject\",\"arg1\":2,\"arg2\":\"1e38967c-7a5701a3\",\"arg3\":true}, {\"ticks\":637795044161966844,\"peer\":\"cbc6f82b-24ec48b3\",\"type\":\"Ubiq.Voip.VoipPeerConnectionManager\",\"objectid\":\"cbc6f82b-24ec48b3\",\"componentid\":50,\"event\":\"CreatePeerConnectionForPeer\",\"arg1\":\"2a865340-80169ce2\",\"arg2\":\"4641730f-148936d7\"}, {\"ticks\":637795044162026839,\"peer\":\"cbc6f82b-24ec48b3\",\"type\":\"Ubiq.Voip.VoipPeerConnectionManager\",\"objectid\":\"cbc6f82b-24ec48b3\",\"componentid\":50,\"event\":\"RequestPeerConnection\",\"arg1\":\"2a865340-80169ce2\",\"arg2\":\"4641730f-148936d7\"}, {\"ticks\":637795044162066856,\"peer\":\"cbc6f82b-24ec48b3\",\"type\":\"Ubiq.Samples.NetworkSpawner\",\"objectid\":\"7725a971-a3692643\",\"componentid\":49018,\"event\":\"SpawnObject\",\"arg1\":2,\"arg2\":\"effadbc0-a6beab2b\",\"arg3\":false}, {\"ticks\":637795043937235620,\"peer\":\"4641730f-148936d7\",\"type\":\"Ubiq.Messaging.NetworkScene\",\"event\":\"Awake\",\"arg1\":\"Oculus Quest\",\"arg2\":\"Oculus Quest\",\"arg3\":\"b8db4746286db62ecad4c6fa13f17ab6\"}, {\"ticks\":637795044080181550,\"peer\":\"4641730f-148936d7\",\"type\":\"Ubiq.Samples.NetworkSpawner\",\"objectid\":\"7725a971-a3692643\",\"componentid\":49018,\"event\":\"SpawnObject\",\"arg1\":2,\"arg2\":\"effadbc0-a6beab2b\",\"arg3\":true}, {\"ticks\":637795044152929360,\"peer\":\"4641730f-148936d7\",\"type\":\"Ubiq.Voip.VoipPeerConnectionManager\",\"objectid\":\"4641730f-148936d7\",\"componentid\":50,\"event\":\"CreatePeerConnectionForRequest\",\"arg1\":\"2a865340-80169ce2\"}, {\"ticks\":637795044153061850,\"peer\":\"4641730f-148936d7\",\"type\":\"Ubiq.Samples.NetworkSpawner\",\"objectid\":\"7725a971-a3692643\",\"componentid\":49018,\"event\":\"SpawnObject\",\"arg1\":2,\"arg2\":\"1e38967c-7a5701a3\",\"arg3\":false} ] Graceful Exit The Questionnaire Panel also has a Quit button. This button makes use of the LogCollector WaitForTransmitComplete method to quit the application, but only when the questionnaire results have been successfully delivered. public void Quit() { LogCollector.Find(this).WaitForTransmitComplete(results.EventType, ready => { if(!ready) { // Here it may be desirable to to save the logs another way Debug.LogWarning(\"ActiveCollector changed or went away: cannot confirm logs have been delivered!\"); } #if UNITY_EDITOR UnityEditor.EditorApplication.isPlaying = false; #else Application.Quit(); #endif }); } The callback will only be called once the Experiment logs have left the local LogCollector. Enter Play Mode, click Done, then click Quit. Since the LogCollector is buffering, the application won't exit because the Log Events are still in Memory. Click Start Collection and the application will immediately write the logs and exit. Try as well again entering Play Mode, and clicking Done and Quit. This time however join a Room with a LogCollector on another Peer. As soon as that Peer's LogCollector is Started, the Questionnaire will quit. If the LogCollector on the other Peer is already active (e.g. in the case of a running logcollectorservice), the application will quit almost as soon as it joins the Room.","title":"Questionnaire"},{"location":"eventloggingexperimentquestionnaire/#questionnaire","text":"The Questionnaire Sample (Samples/Single/Questionnaire) shows how the Event Logging System may be used to collect questionnaire responses. This scene contains a panel with an example Component, Questionnaire attached to it. The Component iterates over all Slider instances under its GameObject , and uses an ExperimentLogEmitter to write their values when the user clicks Done . public class Questionnaire : MonoBehaviour { LogEmitter results; // Start is called before the first frame update void Start() { results = new ExperimentLogEmitter(this); } public void Done() { foreach (var item in GetComponentsInChildren()) { results.Log(\"Answer\", item.name, item.value); } } } The Questionnaire can be completed locally in Play Mode. Alternatively, the scene can be run remotely, and the experimentor in the Editor can join the same room as the remote copy. In either case, the experimentor in the Editor can click Start Collection on the NetworkScene > Log Manager > LogCollector to recieve the Questionnaire results. The experimentor can click Start Collection before or after the questionnaire has been completed, and the participant can complete the Questionnaire before or after joining the room. In all cases the results will be receieved correctly.","title":"Questionnaire"},{"location":"eventloggingexperimentquestionnaire/#sample-output","text":"Below is the resulting Experiment log file from an application built with the Questionnaire scene. [ {\"ticks\":637795003787005516,\"peer\":\"f7d98080-7c7b05ca\",\"event\":\"Answer\",\"arg1\":\"Slider 1\",\"arg2\":0.707253}, {\"ticks\":637795003787045512,\"peer\":\"f7d98080-7c7b05ca\",\"event\":\"Answer\",\"arg1\":\"Slider 2\",\"arg2\":0.30657154}, {\"ticks\":637795003787045512,\"peer\":\"f7d98080-7c7b05ca\",\"event\":\"Answer\",\"arg1\":\"Slider 3\",\"arg2\":0.7034317} ] The Questionnaire was filled in on an Oculus Quest, after joining the same room as a user running the same scene in the Unity Editor. As soon as the Questionnaire was completed, the Unity Editor user could find the Experiment log by clicking the Open Folder button of the LogCollector Component in the Editor. Since no filters were set up on the LogManager , a Debug log for the session is also created in the same folder. [ {\"ticks\":637795043778071253,\"peer\":\"cbc6f82b-24ec48b3\",\"type\":\"Ubiq.Messaging.NetworkScene\",\"event\":\"Awake\",\"arg1\":\"DESKTOP-F1J0MRR\",\"arg2\":\"System Product Name (ASUS)\",\"arg3\":\"f73fe01b1e21031d49274a1491d1d6b5714c92e9\"}, {\"ticks\":637795044161926844,\"peer\":\"cbc6f82b-24ec48b3\",\"type\":\"Ubiq.Samples.NetworkSpawner\",\"objectid\":\"7725a971-a3692643\",\"componentid\":49018,\"event\":\"SpawnObject\",\"arg1\":2,\"arg2\":\"1e38967c-7a5701a3\",\"arg3\":true}, {\"ticks\":637795044161966844,\"peer\":\"cbc6f82b-24ec48b3\",\"type\":\"Ubiq.Voip.VoipPeerConnectionManager\",\"objectid\":\"cbc6f82b-24ec48b3\",\"componentid\":50,\"event\":\"CreatePeerConnectionForPeer\",\"arg1\":\"2a865340-80169ce2\",\"arg2\":\"4641730f-148936d7\"}, {\"ticks\":637795044162026839,\"peer\":\"cbc6f82b-24ec48b3\",\"type\":\"Ubiq.Voip.VoipPeerConnectionManager\",\"objectid\":\"cbc6f82b-24ec48b3\",\"componentid\":50,\"event\":\"RequestPeerConnection\",\"arg1\":\"2a865340-80169ce2\",\"arg2\":\"4641730f-148936d7\"}, {\"ticks\":637795044162066856,\"peer\":\"cbc6f82b-24ec48b3\",\"type\":\"Ubiq.Samples.NetworkSpawner\",\"objectid\":\"7725a971-a3692643\",\"componentid\":49018,\"event\":\"SpawnObject\",\"arg1\":2,\"arg2\":\"effadbc0-a6beab2b\",\"arg3\":false}, {\"ticks\":637795043937235620,\"peer\":\"4641730f-148936d7\",\"type\":\"Ubiq.Messaging.NetworkScene\",\"event\":\"Awake\",\"arg1\":\"Oculus Quest\",\"arg2\":\"Oculus Quest\",\"arg3\":\"b8db4746286db62ecad4c6fa13f17ab6\"}, {\"ticks\":637795044080181550,\"peer\":\"4641730f-148936d7\",\"type\":\"Ubiq.Samples.NetworkSpawner\",\"objectid\":\"7725a971-a3692643\",\"componentid\":49018,\"event\":\"SpawnObject\",\"arg1\":2,\"arg2\":\"effadbc0-a6beab2b\",\"arg3\":true}, {\"ticks\":637795044152929360,\"peer\":\"4641730f-148936d7\",\"type\":\"Ubiq.Voip.VoipPeerConnectionManager\",\"objectid\":\"4641730f-148936d7\",\"componentid\":50,\"event\":\"CreatePeerConnectionForRequest\",\"arg1\":\"2a865340-80169ce2\"}, {\"ticks\":637795044153061850,\"peer\":\"4641730f-148936d7\",\"type\":\"Ubiq.Samples.NetworkSpawner\",\"objectid\":\"7725a971-a3692643\",\"componentid\":49018,\"event\":\"SpawnObject\",\"arg1\":2,\"arg2\":\"1e38967c-7a5701a3\",\"arg3\":false} ]","title":"Sample Output"},{"location":"eventloggingexperimentquestionnaire/#graceful-exit","text":"The Questionnaire Panel also has a Quit button. This button makes use of the LogCollector WaitForTransmitComplete method to quit the application, but only when the questionnaire results have been successfully delivered. public void Quit() { LogCollector.Find(this).WaitForTransmitComplete(results.EventType, ready => { if(!ready) { // Here it may be desirable to to save the logs another way Debug.LogWarning(\"ActiveCollector changed or went away: cannot confirm logs have been delivered!\"); } #if UNITY_EDITOR UnityEditor.EditorApplication.isPlaying = false; #else Application.Quit(); #endif }); } The callback will only be called once the Experiment logs have left the local LogCollector. Enter Play Mode, click Done, then click Quit. Since the LogCollector is buffering, the application won't exit because the Log Events are still in Memory. Click Start Collection and the application will immediately write the logs and exit. Try as well again entering Play Mode, and clicking Done and Quit. This time however join a Room with a LogCollector on another Peer. As soon as that Peer's LogCollector is Started, the Questionnaire will quit. If the LogCollector on the other Peer is already active (e.g. in the case of a running logcollectorservice), the application will quit almost as soon as it joins the Room.","title":"Graceful Exit"},{"location":"eventlogginggettingstarted/","text":"Logging Ubiq has the ability to record, forward and store logs. Ubiq itself generates logs, and custom components can create them too. For example, the logging system could be used to record the answers to a questionnaire, or the direction of a user's gaze, and forward them to an experimentor. This guide shows how to set up and log some simple data in the Hello World scene. Log Flow Log events (such as answering a question) are generated by Log Emitters with a simple call, e.g. debug.Log(\"MyEvent\") . These events are received by a Log Collector. LogCollector is a Component belonging to a NetworkScene . Depending on where logs are finally written, the local LogCollector may forward events to another LogCollector in the same room, or write them directly to a file. There can be many Log Emitters in an application. There should be one LogCollector per Peer. Only one LogCollector should be writing at a time. Creating a Questionnaire Button Log events can come from any source. In this guide, they will be generated when a user presses a button. Create a new Button in the scene. Below, a new GameObject was added to the Main Menu . Create a new script, ButtonLogger , and add it to the Button as well. The script for ButtonLogger is below. using Ubiq.Logging; using UnityEngine; using UnityEngine.UI; public class ButtonLogger : MonoBehaviour { ExperimentLogEmitter events; // Start is called before the first frame update void Start() { events = new ExperimentLogEmitter(this); GetComponent