> For the complete documentation index, see [llms.txt](https://kinematicsoup.gitbook.io/reactor/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://kinematicsoup.gitbook.io/reactor/tutorials/cluster_data_and_rpcs.md).

# Room-to-Room Data and RPCs

## Tutorial 8 - Cluster Properties and Room to Room RPCs

### Summary

This tutorial introduces cluster properties and uses them to share data between rooms. Messaging between rooms will be demonstrated using cluster RPCs. This tutorial extends Tutorial 7. Cluster properties are key-value pairs that can be accessed by any room connected to the same cluster. You can use cluster properties to store daily or weekly high score data. Cluster RPCs are like client to server or server to client RPCs, except they are sent between rooms connected to the same cluster. User cluster RPCs for one-time messages between rooms, such as sending information about a player that will be moving from one room to another.

#### Requirements

* [Reactor Requirements](/reactor/overview.md#requirements)
* Completion of Tutorials [1](/reactor/tutorials/basics.md), [4](/reactor/tutorials/properties_and_rpcs.md), [6](/reactor/tutorials/public_data.md), and [7](/reactor/tutorials/cluster_room_management.md).

### Setup

1. Start with the 'ClusterTutorial' scene from [Tutorial 7](/reactor/tutorials/cluster_room_management.md).

### Scripting

#### Modify the Consts Script to Add More RPCs and a Property

The consts script needs one more RPC id defined: `SCORE`.

1. Open and edit Consts.cs from [Tutorial 7](/reactor/tutorials/cluster_room_management.md).

*Consts.cs*

```c#
// RPC IDs
public class RPC
{
    public const uint NUM_ROOMS = 0;
    public const uint JOIN_ROOM = 1;
    public const uint SHUT_DOWN = 2;
    public const uint SCORE = 3;
}
```

## Modify the Server Game Room Script to Generate Scores

We're going to modify the server game room script to generate a score when it receives an RPC from a player. It will read high score data from cluster properties. It will broadcast new high scores to all rooms using a room-to-room cluster RPC.

> *Cluster properties can be accessed by any room connected to the same cluster. They are transient and will be lost when the cluster is shutdown.* *Persistent cluster properties is an experimental feature that is disabled. Contact us if you want to test using the experimental cluster properties feature.*

1. Open 'ServerGameRoom.cs' from [Tutorial 7](/reactor/tutorials/cluster_room_management.md).
2. Define a *ksRandom* field.
3. Define an int high score field.
4. Define an `OnGetScoreProperty` function.
5. In the `Initialize` function, fetch the "highScore" property using `Cluster.GetProperty()` and assign the `OnGetScoreProperty` function as the `OnComplete` callback.
   * Cluster properties are a key-value store where the keys are strings and values are `ksMultiTypes`. Properties can be grouped as subproperties by using a "." in the property key. When fetching a property, all subproperties of the property are returned. Eg. if you create properties "A", "A.B", "A.C", and "A.D.E", when you fetch property "A", you'll retrieve properties "A", "A.B", "A.C", and "A.D.E". It is possible for a property with no value to have subproperties (in the above example, there is no property "A.D" but there is an "A.D.E"). Our script fetches two properties grouped together as subproperties of "highScore": "highScore.score" and "highScore.username".
6. Define an `OnScore` function tagged with `[ksRPC(RPC.SCORE)]` to generate a random score when a client sends a score RPC. If the score is a new high score, inform all rooms about the new high score using a room-to-room cluster RPC.
   * Similar to client-to-server and server-to-client RPCs, rooms can call RPCs on other rooms using cluster RPCs by calling `Cluster.CallRoomRPC()`. There are four overloads of this function for specifying which rooms will receive the RPC. One tags an `IEnumerable` of uint room ids to send the RPC to, one takes a string tag that will send the RPC to all rooms with that tag, one that takes an `IEnumerable` of string tags that will send the RPC to all rooms that have all the tags, and one that takes no tag or room id arguments that sends it to all rooms in the cluster (including the caller). Instead of `[ksRPC(id)]`, cluster RPC handler functions are tagged with `[ksClusterRPC(id)]`. Cluster RPC handlers tagged with `[ksClusterRPC(id, false)]` are called off the main-thread.
7. Define an `OnHighScore` function tagged with `[ksClusterRPC(RPC.SCORE)]` to log new high scores that are received via room-to-room cluster RPCs.

*ServerGameRoom.cs*

```c#
using System;
using System.Collections.Generic;
using KS.Reactor.Server;
using KS.Reactor;

public class ServerGameRoom : ksServerRoomScript
{
    private ksRandom m_random = new ksRandom();
    private int m_highScore;

    // Called when the script is attached.
    public override void Initialize()
    {
        // Setting IsPublic to false prevents the room from appearing in the results of ksReactor.GetRooms requests
        // made by clients. We hide the game rooms because we want clients to connect to a lobby room initially.
        Room.IsPublic = false;
        
        // Add the "Game" tag so this room is in the results when the lobby fetches all rooms with the "Game" tag.
        Room.Info.PublicTags.Add("Game");
        
        // Add event listeners.
        Room.OnShutDown += OnShutDown;
        Room.OnPlayerJoin += PlayerJoin;
        Room.OnPlayerLeave += PlayerLeave;
        
        // Set the number of connections in the public data.
        UpdateConnections();

        // Check if we are connected to the cluster.
        if (Cluster.IsConnected)
        {
            // Get the "highScore" property from cluster properties asynchronously. Call OnGetScoreProperty when the
            // request completes.
            Cluster.GetProperty("highScore").OnComplete = OnGetScoreProperty;
        }
    }

    // Called when the script is detached.
    public override void Detached()
    {
        Room.OnShutDown -= OnShutDown;
        Room.OnPlayerJoin -= PlayerJoin;
        Room.OnPlayerLeave -= PlayerLeave;
    }

    // Called when a player connects.
    private void PlayerJoin(ksIServerPlayer player)
    {
        ksLog.Info(player.Id + " joined.");
        
        // Update the number of connections if this player is not a bot.
        if (!player.IsVirtual)
        {
            UpdateConnections();
        }
    }

    // Called when a player disconnects.
    private void PlayerLeave(ksIServerPlayer player)
    {
        ksLog.Info(player.Id + " left.");
        
        // Update the number of connections if this player is not a bot.
        if (!player.IsVirtual)
        {
            UpdateConnections();
        }
    }

    private void UpdateConnections()
    {
        // Set the number of connections in the public data. This is available in the response to Cluster.GetRoomInfo
        // calls as ksRoomInfo.PublicData.
        Room.Info.PublicData["connections"] = Room.ConnectedPlayerCount;
    }

    // Clients can call this RPC to shut down the game room.
    [ksRPC(RPC.SHUT_DOWN)]
    private void ShutDown()
    {
        Room.ShutDown();
    }

    // Log a message before shutting down.
    private void OnShutDown()
    {
        ksLog.Info("Shutting down...");
    }

    // Called asynchronously when our request to retrieve the "highScore" property from cluster properties completes.
    private void OnGetScoreProperty(ksAsyncResult<Dictionary<string, ksMultiType>> result)
    {
        // Error handling.
        if (!string.IsNullOrEmpty(result.Error))
        {
            ksLog.Error("Error getting highScore property: " + result.Error);
            return;
        }
        // Get the subproperties "highScore.score" and "highScore.username".
        ksMultiType value;
        if (result.Result.TryGetValue("highScore.score", out value))
        {
            m_highScore = value;
        }
        if (result.Result.TryGetValue("highScore.username", out value))
        {
            ksLog.Info("High score: " + m_highScore + " by " + value);
        }
    }

    // Called when a player sends a score RPC.
    [ksRPC(RPC.SCORE)]
    private void OnScore(ksIServerPlayer player, string name)
    {
        // Assign the player a random score.
        int score = Math.Min(m_random.Next(10000), m_random.Next(10000));
        ksLog.Info(name + " scored " + score);
        
        // If it's higher than the high score, send a cluster RPC to inform all rooms about the new high score.
        if (score > m_highScore)
        {
            m_highScore = score;
            Cluster.CallRoomRPC(RPC.SCORE, score, name);
        }
    }

    // Called when we receive a room-to-room cluster RPC informing us of a new high score.
    // The first parameter is the id of the room that sent the RPC.
    [ksClusterRPC(RPC.SCORE)]
    private void OnHighScore(uint roomId, int score, string name)
    {
        m_highScore = score;
        ksLog.Info("New high score: " + score + " by " + name);
    }
}
```

#### Modify the Server Lobby Room Script to Handle High Scores

We're going to modify the server lobby room script to read and write high score data to cluster properties.

1. Open 'ServerLobbyRoom.cs' from [Tutorial 7](/reactor/tutorials/cluster_room_management.md).
2. Define an `OnGetScoreProperty` function.
3. In the `Initialize function`, fetch the "highScore.score" property from cluster properties using `Cluster.GetProperty()` and assign the `OnGetScoreProperty` function as the `OnComplete` callback.
4. Define an `OnHighScore` function tagged with `[ksClusterRPC(RPC.SCORE)]` to log new high scores that are received via room-to-room cluster RPCs and write them to cluster properties.

*ServerLobbyRoom.cs*

```c#
using System;
using System.Collections.Generic;
using KS.Reactor.Server;
using KS.Reactor;

public class ServerLobbyRoom : ksServerRoomScript
{
    // How often to refresh the list of game rooms.
    [ksEditable] public float RefreshInterval = 10f;

    private float m_refreshTimer;
    private List<ksRoomInfo> m_rooms = new List<ksRoomInfo>();
    
    // Object for locking access to m_rooms.
    private object m_roomsLock = new object();
    
    // The desired number of game rooms.
    private int m_targetRoomCount = 1;
    private ksRandom m_random = new ksRandom();

    private long m_highScore;

    // Called when the script is attached.
    public override void Initialize()
    {
        Room.OnUpdate[0] += Update;
        m_refreshTimer = 0f;

        // Check if we are connected to the cluster.
        if (Cluster.IsConnected)
        {
            // Get the "highScore.Score" property from cluster properties asynchronously. Call OnGetScoreProperty when
            // the request completes.
            Cluster.GetProperty("highScore.Score").OnComplete = OnGetScoreProperty;
        }
    }

    // Called when the script is detached.
    public override void Detached()
    {
        Room.OnUpdate[0] -= Update;
    }

    // Called during the update cycle.
    private void Update()
    {
        if (!Cluster.IsConnected)
        {
            // Do nothing if we are not connected to the cluster.
            return;
        }

        // Refresh the list of game rooms when the timer reaches zero.
        m_refreshTimer -= Time.Delta;
        if (m_refreshTimer <= 0)
        {
            // The parameter "Game" filters the results to only include rooms with the tag "Game", thereby excluding
            // this room from the results.
            Cluster.GetRoomInfo("Game").OnComplete += OnGetRooms;
            m_refreshTimer = RefreshInterval;
        }
    }

    // Called asynchronously when our request to retrieve the "highScore.score" property from cluster properties
    // completes.
    private void OnGetScoreProperty(ksAsyncResult<Dictionary<string, ksMultiType>> result)
    {
        // Error handling.
        if (!string.IsNullOrEmpty(result.Error))
        {
            ksLog.Error("Error getting highScore.score property: " + result.Error);
            return;
        }
        // Get the property. Since properties can have any number of sub-properties and all sub-properties are returned
        // when fetching a property, the result is a dictionary containing all returned properties.
        ksMultiType value;
        if (result.Result.TryGetValue("highScore.score", out value))
        {
            m_highScore = value;
        }
    }

    // Called when the GetRoomInfo request completes.
    private void OnGetRooms(ksAsyncResult<List<ksRoomInfo>> result)
    {
        // Error handling
        if (!string.IsNullOrEmpty(result.Error))
        {
            ksLog.Error("Error getting rooms: " + result.Error);
            return;
        }
        // Completion callbacks for Cluster operations are called off the main thread, so we use a lock to make
        // accessing m_rooms thread-safe.
        lock (m_roomsLock)
        {
            m_rooms = result.Result;
            int numPlayers = Room.ConnectedPlayerCount;
            
            // Stop rooms that have no connected players until we reach the target room count.
            int numToStop = m_rooms.Count - m_targetRoomCount;
            
            // Create a list of room ids to stop.
            List<uint> roomsToStop = new List<uint>();
            foreach (ksRoomInfo roomInfo in m_rooms)
            {
                // Get the number of connected players from the public data.
                int numConnections = roomInfo.PublicData["connections"];
                
                // Add the number of connections to the total player count.
                numPlayers += numConnections;
                
                // Add the room id to the stop list if it has no players and we need to stop rooms.
                if (roomsToStop.Count < numToStop && numConnections == 0)
                {
                    ksLog.Info("Stopping room " + roomInfo.Id);
                    roomsToStop.Add(roomInfo.Id);
                }
            }
            // Log the number of rooms and players.
            ksLog.Info("Rooms: " + m_rooms.Count + ", Players: " + numPlayers);
            
            // Stop the rooms in the list.
            if (roomsToStop.Count > 0)
            {
                Cluster.StopRoom(roomsToStop.ToArray());
            }
            // Launch new game rooms until we reach the target number of rooms.
            for (int i = m_rooms.Count; i < m_targetRoomCount; i++)
            {
                // The first parameter is the name of the room, which can be anything.
                // The second parameter is the name of the scene to launch, and third is the room type name.
                // These must match your Unity scene name and ksRoomType game object name.
                // Call OnStartRoom when the request completes.
                Cluster.StartRoom("Game" + i, "ClusterTutorial", "GameRoom").OnComplete = OnStartRoom;
            }
        }
    }

    private void OnStartRoom(ksAsyncResult<ksRoomInfo> result)
    {
        if (!string.IsNullOrEmpty(result.Error))
        {
            // Error handling.
            ksLog.Error("Error starting room: " + result.Error);
        }
        else
        {
            // Log the id of the started room.
            ksLog.Info("Started room " + result.Result.Id);
        }
    }

    // Clients can call this RPC to set the target number of game rooms.
    [ksRPC(RPC.NUM_ROOMS)]
    private void OnSetNumRooms(int numRooms)
    {
        // Clamp the target number between 0 and 3.
        numRooms = Math.Min(numRooms, 3);
        numRooms = Math.Max(numRooms, 0);
        ksLog.Info("Set target room count to " + numRooms);
        m_targetRoomCount = numRooms;
    }

    // Clients call this RPC when they want to join a game room.
    [ksRPC(RPC.JOIN_ROOM)]
    private void OnRequestJoinRoom(ksIServerPlayer player)
    {
        // Use a lock so we can access m_rooms thread-safely.
        lock (m_roomsLock)
        {
            if (m_rooms.Count == 0)
            {
                // Calling the JOIN_ROOM RPC with no arguments tells the client there are no game rooms to join.
                Room.CallRPC(player, RPC.JOIN_ROOM);
            }
            else
            {
                // Choose a random game room to send the client to.
                int index = m_random.Next(m_rooms.Count);
                
                // Send the ksRoomInfo to the client.
                Room.CallRPC(player, RPC.JOIN_ROOM, m_rooms[index]);
            }
        }
    }

    // Called when we receive a room-to-room cluster RPC informing us of a new high score.
    [ksClusterRPC(RPC.SCORE)]
    private void OnHighScore(uint roomId, int score, string name)
    {
        // Check that this score is higher than the current high score.
        if (score > m_highScore)
        {
            m_highScore = score;
            ksLog.Info("New high score: " + score + " by " + name);

            // Store the high score and username of the player who got the score in cluster properties.
            // A dictionary can be used to set multiple properties at a time.
            Dictionary<string, ksMultiType> scoreProperties = new Dictionary<string, ksMultiType>();
            scoreProperties["highScore.score"] = score;
            scoreProperties["highScore.username"] = name;
            Cluster.SetProperty(scoreProperties);
        }
    }
}
```

#### Modify the Client Game Room Script to Request a Score When Space is Pressed

We're going to make the client game room script call the score RPC on the server when **space** is pressed.

1. Open 'ClientGameRoom.cs' from [Tutorial 7](/reactor/tutorials/cluster_room_management.md).
2. Call the `SCORE` RPC with your username when **space** is pressed in the `Update` function.

*ClientGameRoom.cs*

```c#
    ...
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.L) && Connect.Instance != null)
        {
            // When L is pressed, return to the lobby.
            Connect.Instance.ConnectToServer();
        }
        else if (Input.GetKeyDown(KeyCode.S))
        {
            // When S is pressed, call an RPC to shut down the game room and return to the lobby once the room is shut
            // down.
            Room.CallRPC(RPC.SHUT_DOWN);
        }
        else if (Input.GetKeyDown(KeyCode.Space))
        {
            // Request a score. The parameter is your username. You can change this to anything.
            Room.CallRPC(RPC.SCORE, "MyName");
        }
    }
    ...
```

### Testing

1. Start a local cluster running the 'ClusterTutorial' scene and 'LobbyRoom' room type.
2. Check the 'Use Local Server' checkbox in the inspector for the 'Connect' script.
3. Enter play mode.
4. Press **J** to enter a game room.
   * You may see a message saying there are no rooms to join if the lobby hasn't started a game room yet. Wait a bit and try again.

When you press **space** while in a game room, the server will log a random score for you. If it's a new high score, the high score and your name will appear in the logs for all running rooms. When you start a new game room, the game room's logs will log the current high score and the username of the player who set the score.
