Skip to content
Open
1 change: 1 addition & 0 deletions Library/include/CSP/CSPFoundation.h
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class CSP_API EndpointURIs
ServiceDefinition AggregationService;
ServiceDefinition TrackingService;
ServiceDefinition MaintenanceWindow;
ServiceDefinition MultiplayerConnection;
};

/// @brief Holds client data used in requests for all Magnopus Serives.
Expand Down
98 changes: 98 additions & 0 deletions Library/include/CSP/Systems/Multiplayer/MultiplayerSystem.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2025 Magnopus LLC

* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#pragma once

#include "CSP/CSPCommon.h"
#include "CSP/Systems/Multiplayer/Scope.h"
#include "CSP/Systems/Multiplayer/ScopeLeader.h"
#include "CSP/Systems/SystemBase.h"

#include <memory>

namespace csp::services
{

class ApiBase;

} // namespace csp::services

namespace csp::web
{

class WebClient;

} // namespace csp::web

namespace csp::systems
{
class SpaceSystem;

/// @ingroup Multiplayer System
/// @brief Public facing system that allows interfacing with Magnopus Connected Services' multiplayer api.
/// Offers methods for managing realtime state via REST calls.
class CSP_API CSP_NO_DISPOSE MultiplayerSystem : public SystemBase
{
public:
CSP_NO_EXPORT MultiplayerSystem(csp::web::WebClient* WebClient, csp::systems::SpaceSystem& SpaceSystem, csp::common::LogSystem& LogSystem);
MultiplayerSystem(); // This constructor is only provided to appease the wrapper generator and should not be used
~MultiplayerSystem();

/// @brief Gets all scopes associated with the given space id.
/// Note: These functions are currently not exported, as they are only used for testing, as we haven't fully implemented scopes within csp.
/// @param SpaceId const csp::common::String& : The id of the space we want to get scopes for.
/// @param Callback csp::systems::ScopesResultCallback : Callback when the scopes are retrieved, or on failure.
/// @pre Must already have entered the space of the SpaceId parameter.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Consider] Stating what happens when this precondition is violated.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in: 2553f80

Copy link
Contributor

@MAG-ElliotMorris MAG-ElliotMorris Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Comment] I still think this erroring behaviour is poor, an empty array of scopes is not a good marker for "You were not in the space", it could be caused by anything. I'm presuming we don't get a helpful error from CHS telling us why the request failed, which is what I'd ideally like.

[Question[ To give better errors, would we have to query CHS again to check if we're in the space beforehand? Seems like context the forwarded CHS response should be able to provide.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in: 9860b3d

Used the space system to check if we're in the space. We do this for HotspotSequenceSystem, as well.

/// A CSP error will be sent to the LogSystem if this condition is not met, with a EResultCode::Failed response.
CSP_NO_EXPORT void GetScopesBySpace(const csp::common::String& SpaceId, ScopesResultCallback Callback);

/// @brief Updates Data on a scope.
/// Note: These functions are currently not exported, as they are only used for testing, as we haven't fully implemented scopes within csp.
/// @param ScopeId const csp::common::String& : The id of the scope we want to update.
/// @param Scope const csp::systems::Scope& : Scope containing new values for the given id.
/// @param Callback csp::systems::ScopesResultCallback : Callback when asynchronous task finishes.
CSP_NO_EXPORT void UpdateScopeById(const csp::common::String& ScopeId, const csp::systems::Scope& Scope, ScopeResultCallback Callback);

/// @brief Gets details about a scope leader.
/// Note: These functions are currently not exported, as they are only used for testing, as we haven't fully implemented scopes within csp.
/// @param ScopeId const csp::common::String& : The id of the scope we want to get scope leader details about.
/// @param Callback csp::systems::ScopesResultCallback : Callback when asynchronous task finishes.
/// @pre "ManagedLeaderElection" should be set to true on the scope, otherwise this function will fail with a EResultCode::Failed response.
/// @pre Must already have entered the space of the SpaceId parameter.
CSP_NO_EXPORT void GetScopeLeader(const csp::common::String& ScopeId, ScopeLeaderResultCallback Callback);

/// @brief Starts leader election for the given scope.
/// Note: These functions are currently not exported, as they are only used for testing, as we haven't fully implemented scopes within csp.
/// This should not need to be called outside of testing, as leader election is done automatically.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Consider] Maybe we should do the __ thing for the naming that we've been talking about for this? Heck, i'd even feature flag it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in: 2553f80 by setting these types NO_EXPORT, until we understand how scopes are going to be used in CSP

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Question] I was under the impression we agreed on the __ naming convention for methods that only make sense in a test context going forwards. Am I mistaken in my understanding of either our new standard or what this method is? We can't just agree to do things and then not do them. I'm fine with this code, but you'll need to bring a reason why you didn't want to use the naming convention to the next sync.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in: 16e3767

/// @param ScopeId const csp::common::String& : The id of the scope we want to start leader election for.
/// @param UserIdsToExclude const csp::common::Optional<csp::common::Array<csp::common::String>>& : A list of user ids we don't want to consider
/// for election.
/// @param Callback csp::systems::ScopesResultCallback : Callback when asynchronous task finishes.
/// @pre "ManagedLeaderElection" should be set to true on the scope, otherwise this function will fail with a EResultCode::Failed response.
/// @pre Must already have entered the space of the SpaceId parameter.
CSP_NO_EXPORT void __PerformLeaderElectionInScope(const csp::common::String& ScopeId,
const csp::common::Optional<csp::common::Array<csp::common::String>>& UserIdsToExclude, NullResultCallback Callback);

private:
CSP_START_IGNORE
std::unique_ptr<csp::services::ApiBase> ScopeLeaderApi;
std::unique_ptr<csp::services::ApiBase> ScopesApi;
CSP_END_IGNORE

csp::systems::SpaceSystem* SpaceSystem;
};

}
142 changes: 142 additions & 0 deletions Library/include/CSP/Systems/Multiplayer/Scope.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright 2025 Magnopus LLC

* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#pragma once

#include "CSP/CSPCommon.h"
#include "CSP/Systems/SystemsResult.h"
#include "CSP/Systems/WebService.h"

#include <functional>

namespace csp::services
{

CSP_START_IGNORE
template <typename T, typename U, typename V, typename W> class ApiResponseHandler;
CSP_END_IGNORE

} // namespace csp::services

namespace csp::services::generated::multiplayerservice
{
class ScopeDto;
}

namespace csp::systems
{

/// @ingroup Multiplayer System
/// @brief Enum representing the scopes pub/sub model type
/// Object: used in object scopes, each object is published to its own channel, and
/// client subscribes to the channels of only the objects they can see
/// Global: used in global scopes, all objects are published to a single channel,
/// client subscribes to the channel and can see everything in the channel/scope
enum class PubSubModelType
{
Object,
Global
};

/// @ingroup Multiplayer System
/// @brief Data representation for a scope in a space.
/// Scopes represent different channels in a space which objects can exist in.
/// This allows csp/mcs to only reason about objects in specific scopes.
class CSP_API Scope
{
public:
/// @brief The unique identifier of the scope.
/// This is set internally by MCS.
csp::common::String Id;
/// @brief The id of the object this scope relates to.
/// This is currently always the space id.
csp::common::String ReferenceId;
/// @brief The type of object this scope relates to.
/// This is currently always "GroupId", as it references the space.
csp::common::String ReferenceType;
/// @brief The name of the scope, this should be a human readable string to identify the scope.
csp::common::String Name;
/// @brief The pub/sub model of the scope.
/// This allows us define a global scope for the entire space, or a scope with a position and size.
/// See csp::systems::PubSubModelType for more details.
PubSubModelType PubSubType = PubSubModelType::Global;
/// @brief Determines the size of the scope using the radius from the object in meters.
/// This is only used when PubSubType is set to "Object".
double SolveRadius = 0.0;
/// @brief Determines whether server side leader election is enabled on this scope.
/// If this is true, MCS will automatically determine the leader for this scope.
bool ManagedLeaderElection = false;
};

void DtoToScope(const csp::services::generated::multiplayerservice::ScopeDto& Dto, csp::systems::Scope& ScopeLeader);

/// @ingroup Multiplayer System
/// @brief Contains details about an async operation which returns a scope.
/// If the ResultCode is successful, this will contain a valid scope.
class CSP_API ScopeResult : public ResultBase
{
/** @cond DO_NOT_DOCUMENT */
CSP_START_IGNORE
template <typename T, typename U, typename V, typename W> friend class csp::services::ApiResponseHandler;
CSP_END_IGNORE
/** @endcond */

public:
/// @brief Returns the scope if this result is successful.
/// @return const Scope& : The scope retrieved by this result.
const Scope& GetScope() const;

CSP_NO_EXPORT ScopeResult(csp::systems::EResultCode ResCode, uint16_t HttpResCode);

private:
ScopeResult(void*) {};

void OnResponse(const csp::services::ApiResponseBase* ApiResponse) override;

Scope Scope;
};

typedef std::function<void(const ScopeResult& Result)> ScopeResultCallback;

/// @ingroup Multiplayer System
/// @brief Contains details about an async operation which returns an array of scopes.
/// If the ResultCode is successful, this will contain a valid array of scopes.
class CSP_API ScopesResult : public ResultBase
{
/** @cond DO_NOT_DOCUMENT */
CSP_START_IGNORE
template <typename T, typename U, typename V, typename W> friend class csp::services::ApiResponseHandler;
CSP_END_IGNORE
/** @endcond */

public:
/// @brief Returns the an array of scopes if this result is successful.
/// @return const csp::common::Array<Scope>& : The array of scopes retrieved by this result.
const csp::common::Array<Scope>& GetScopes() const;

CSP_NO_EXPORT ScopesResult(csp::systems::EResultCode ResCode, uint16_t HttpResCode);

private:
ScopesResult(void*) {};

void OnResponse(const csp::services::ApiResponseBase* ApiResponse) override;

csp::common::Array<Scope> Scopes;
};

typedef std::function<void(const ScopesResult& Result)> ScopesResultCallback;

}
84 changes: 84 additions & 0 deletions Library/include/CSP/Systems/Multiplayer/ScopeLeader.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright 2025 Magnopus LLC

* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#pragma once

#include "CSP/CSPCommon.h"
#include "CSP/Systems/SystemsResult.h"
#include "CSP/Systems/WebService.h"

#include <functional>

namespace csp::services
{

CSP_START_IGNORE
template <typename T, typename U, typename V, typename W> class ApiResponseHandler;
CSP_END_IGNORE

} // namespace csp::services

namespace csp::services::generated::multiplayerservice
{
class ScopeLeaderDto;
}

namespace csp::systems
{

/// @ingroup Multiplayer System
/// @brief Data representation for a scope leader.
/// A scope leader represents a user which owns a specific scope in a space.
/// The scope leader will run scripts and other operations for the scope.
class CSP_API ScopeLeader
{
public:
/// @brief The scope id the client is leader of.
csp::common::String ScopeId;
/// @brief The client id which is the leader.
csp::common::String ScopeLeaderUserId;
/// @brief Whether there is a server side election currently in progress when this object is received.
bool ElectionInProgress = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[fix] I won't markup every undocumented property.

I'm not trying to be docs mad, I understand in these DTO types some stuff is obvious, but here for example, ElectionInProgress demands explanation. I'm presuming it's in-progress at the time the DTO was generated, folks might mistake it for a live marker property. Does this being true mean the user should assume the ScopeLeader is about to change and should re-query? etc, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in: 2553f80

Copy link
Contributor

@MAG-ElliotMorris MAG-ElliotMorris Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Consider] This still isn't good enough.

/// @brief Whether there is a server side election currently in progress when this object is received.

and

bool ElectionInProgress = false;

mean practically the same thing.

Think about if you were integrating against our Api. Think of a reason why you would call GetScopeLeader, and you got a result with ElectionInProgress = true on it, what would that mean to you, how are you supposed to react? Is the scope leader you have queried about imminently about to be dethroned? Is it guarenteed they will be removed, or just a possibility? Will there be an event when the election is finished, what should I do about that?

I suspect the answer is that you just can't know, as it is with all the other docs, and the NO_EXPORT "strategy".

@MAG-SamBirley @MAG-AdamThorn It is quite frustrating to have to direct my issues at Matt here, it's not his failing. All this is a consequence from CHS not having adequate api documentation that considers workflows, as they are the only people who can answer these sorts of questions. There's only so far we can go with documenting Api's like this and it's difficult to impossible to maintain quality standards in this environment. 😢

I'm tempted to just give up on documentation for the sinkhole REST endpoints, or at least the stuff that CSP dosen't insert opinions on (most of it). It's a losing battle.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I can't answer the questions above. What makes it more difficult is that CHS are still making more changes to this on the backend

};

void DtoToScopeLeader(const csp::services::generated::multiplayerservice::ScopeLeaderDto& Dto, csp::systems::ScopeLeader& ScopeLeader);

/// @ingroup Multiplayer System
/// @brief Contains details about an async operation which returns a scope leader.
/// If the ResultCode is successful, this will contain a valid scope leader.
class CSP_API ScopeLeaderResult : public ResultBase
{
/** @cond DO_NOT_DOCUMENT */
CSP_START_IGNORE
template <typename T, typename U, typename V, typename W> friend class csp::services::ApiResponseHandler;
CSP_END_IGNORE
/** @endcond */

public:
/// @brief Returns the scope leader if this result is successful.
/// @return const ScopeLeader& : The scope leader retrieved by this result.
const ScopeLeader& GetScopeLeader() const;

private:
ScopeLeaderResult(void*) {};

void OnResponse(const csp::services::ApiResponseBase* ApiResponse) override;

ScopeLeader Leader;
};

typedef std::function<void(const ScopeLeaderResult& Result)> ScopeLeaderResultCallback;
}
4 changes: 4 additions & 0 deletions Library/include/CSP/Systems/SystemsManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class HotspotSequenceSystem;
class ConversationSystemInternal;
class AnalyticsSystem;
class ExternalServiceProxySystem;
class MultiplayerSystem;

} // namespace csp::systems

Expand Down Expand Up @@ -169,6 +170,8 @@ class CSP_API SystemsManager
/// @return ExternalServiceProxySystem : pointer to the external services proxy system class.
ExternalServiceProxySystem* GetExternalServicesProxySystem();

MultiplayerSystem* GetMultiplayerSystem();

csp::multiplayer::MultiplayerConnection* GetMultiplayerConnection();

csp::multiplayer::NetworkEventBus* GetEventBus();
Expand Down Expand Up @@ -220,6 +223,7 @@ class CSP_API SystemsManager
ConversationSystemInternal* ConversationSystem;
AnalyticsSystem* AnalyticsSystem;
ExternalServiceProxySystem* ExternalServiceProxySystem;
MultiplayerSystem* MultiplayerSystem;
};

} // namespace csp::systems
Loading