diff --git a/docs/godot/channels.mdx b/docs/godot/channels.mdx index 32e624f..195b888 100644 --- a/docs/godot/channels.mdx +++ b/docs/godot/channels.mdx @@ -250,7 +250,7 @@ await Talo.channels.set_storage_props(channel.id, { }) ``` -This method accepts a dictionary of prop keys and values. You can set a prop value to `null` to delete it. Storage props that aren't being deleted will be upserted (updated if they exist, otherwise created). +This function accepts a dictionary of prop keys and values. You can set a prop value to `null` to delete it. Storage props that aren't being deleted will be upserted (updated if they exist, otherwise created). #### Handling failures diff --git a/docs/godot/player-relationships.mdx b/docs/godot/player-relationships.mdx new file mode 100644 index 0000000..74f45a8 --- /dev/null +++ b/docs/godot/player-relationships.mdx @@ -0,0 +1,274 @@ +--- +sidebar_position: 17 +description: Player relationships allow you to link multiple players together for social features like friends lists, parties and guilds. +--- + +import { ScopeBadges } from '@site/src/components/ScopeBadges' + +# Player relationships + +## Overview + +The Player Relationships API creates subscriptions between players. When a player broadcasts a message, all their subscribers receive it instantly. + +This flexible system supports both social features (friends lists with bidirectional relationships, follower systems with unidirectional relationships) and event-driven mechanics. Since broadcasts accept any data that can be converted to a string (text, JSON-encoded objects, etc.), you can build pub/sub systems where players react to each other's actions - like levelling up, unlocking achievements, or completing challenges. + +### Friends list demo + +A complete sample is available in the Godot plugin at `addons/talo/samples/friends_list`. This demo showcases: + +- Sending and accepting friend requests +- Real-time presence updates showing online friends +- Broadcasting messages between friends + +## Subscribing to players + + + +To subscribe to a player, you need to first decide what type of relationship you want to create: unidirectional or bidirectional. Unidirectional relationships create a subscription to the target player and bidirectional relationships will create a reciprocal subscription between both players. + +You also need to know the target player alias' ID. You can get this through [player presence updates](/docs/godot/player-presence), or by searching for players using [player search](/docs/godot/player-props#searching-for-players). + +Once you have both of these pieces of information, you can create the relationship: + +```gdscript +var search_page := await Talo.players.search("target_player_identifier") +var target_alias_id := search_page.players[0].get_alias().id # this assumes we found at least one player + +# unidirectional relationship +await Talo.player_relationships.subscribe_to(target_alias_id, TaloPlayerAliasSubscription.RelationshipType.UNIDIRECTIONAL) + +# or, bidirectional relationship +await Talo.player_relationships.subscribe_to(target_alias_id, TaloPlayerAliasSubscription.RelationshipType.BIDIRECTIONAL) +``` + +This creates an **unconfirmed** subscription (or two for bidirectional relationships). Players will need to confirm subscription requests before broadcasts can be received by the subscribers. + +## Confirming subscriptions + + + +If you know the player alias ID of the player that sent you a subscription request, you can confirm it using `Talo.player_relationships.confirm_subscription_from()`: + +```gdscript +func _ready(): + # players receive this signal when they get a subscription request + Talo.player_relationships.relationship_request_received.connect(_on_relationship_request_received) + +func _on_relationship_request_received(player_alias: TaloPlayerAlias): + await Talo.player_relationships.confirm_subscription_from(player_alias.id) +``` + +Alternatively, you can list all pending subscription requests using `Talo.player_relationships.get_subscribers()`. + +This function allows you to filter by unconfirmed subscription requests. You can iterate over the results and confirm each subscription individually using their ID: + +```gdscript +var options := Talo.player_relationships.GetSubscribersOptions.new() +options.confirmed = Talo.player_relationships.ConfirmedFilter.UNCONFIRMED +var subscribers_page := await Talo.player_relationships.get_subscribers(options) + +# loop through all unconfirmed subscriptions and confirm them +for subscriber in subscribers_page.subscriptions: + await Talo.player_relationships.confirm_subscription_by_id(subscriber.id) +``` + +## Broadcasting messages + + + +Once relationships have been established, players can communicate with each other using broadcasts. When a player broadcasts a message, it is delivered to all of their confirmed subscribers (in a future release, Talo will support private/targeted broadcasts). + +To send a broadcast, use `Talo.player_relationships.broadcast()`: + +```gdscript +# send a simple text message +Talo.player_relationships.broadcast("Hello everyone!") + +# send structured data as JSON +var event_data = { + "event": "level_up", + "level": 15, + "timestamp": Time.get_unix_time_from_system() +} +Talo.player_relationships.broadcast(JSON.stringify(event_data)) +``` + +Subscribers will receive these broadcasts via the `message_received` signal (see [Signals](#signals) below). + +## Unsubscribing + + + +Only subscribers can revoke subscriptions - the player being subscribed to cannot remove their subscribers. When a bidirectional subscription is revoked, the reciprocal relationship is automatically deleted. + +You can unsubscribe in two ways: + +**By player alias ID** (recommended for most cases): + +```gdscript +# unsubscribe from a player +await Talo.player_relationships.unsubscribe_from(target_alias_id) +``` + +**By subscription ID** (if you already have the subscription object): + +```gdscript +# revoke a specific subscription +await Talo.player_relationships.revoke_subscription(subscription.id) +``` + +## Querying relationships + + + +### Checking subscription status + +Use `is_subscribed_to()` to check if the current player has a subscription to another player: + +```gdscript +# check if subscribed (any status) +var is_subscribed := await Talo.player_relationships.is_subscribed_to(target_alias_id, false) + +# check if subscribed AND confirmed +var is_confirmed := await Talo.player_relationships.is_subscribed_to(target_alias_id, true) +``` + +### Listing subscriptions + +Get all players that the current player is subscribed to using `get_subscriptions()`: + +```gdscript +# get all subscriptions +var page := await Talo.player_relationships.get_subscriptions() +for subscription in page.subscriptions: + print("Subscribed to: %s" % subscription.subscribed_to.identifier) +``` + +You can filter the results using options: + +```gdscript +var options := Talo.player_relationships.GetSubscriptionsOptions.new() + +# filter by confirmation status +options.confirmed = Talo.player_relationships.ConfirmedFilter.CONFIRMED # only confirmed +options.confirmed = Talo.player_relationships.ConfirmedFilter.UNCONFIRMED # only unconfirmed +options.confirmed = Talo.player_relationships.ConfirmedFilter.ANY # all (default) + +# filter by specific player alias ID +options.alias_id = target_alias_id + +# filter by relationship type +options.relationship_type = Talo.player_relationships.RelationshipTypeFilter.UNIDIRECTIONAL +options.relationship_type = Talo.player_relationships.RelationshipTypeFilter.BIDIRECTIONAL +options.relationship_type = Talo.player_relationships.RelationshipTypeFilter.ANY # all (default) + +# pagination +options.page = 0 # page number (default: 0) + +var page := await Talo.player_relationships.get_subscriptions(options) +``` + +### Listing subscribers + +Get all players subscribed to the current player using `get_subscribers()`: + +```gdscript +# get all subscribers +var page := await Talo.player_relationships.get_subscribers() +for subscription in page.subscriptions: + print("Subscriber: %s" % subscription.subscriber.identifier) +``` + +This function accepts the same filter options as `get_subscriptions()`: + +```gdscript +var options := Talo.player_relationships.GetSubscribersOptions.new() + +# filter by confirmation status +options.confirmed = Talo.player_relationships.ConfirmedFilter.UNCONFIRMED # pending requests + +# filter by specific player alias ID +options.alias_id = subscriber_alias_id + +# filter by relationship type +options.relationship_type = Talo.player_relationships.RelationshipTypeFilter.BIDIRECTIONAL + +# pagination +options.page = 1 + +var page := await Talo.player_relationships.get_subscribers(options) +``` + +## Signals + + + +The Player Relationships API emits signals for real-time relationship events. Connect to these signals to respond to relationship changes and incoming messages: + +### relationship_request_received + +Emitted when another player sends the current player a relationship request. + +```gdscript +func _ready(): + Talo.player_relationships.relationship_request_received.connect(_on_relationship_request_received) + +func _on_relationship_request_received(player_alias: TaloPlayerAlias): + print("%s wants to connect with you" % player_alias.identifier) + # optionally auto-confirm + await Talo.player_relationships.confirm_subscription_from(player_alias.id) +``` + +### relationship_request_cancelled + +Emitted when an unconfirmed relationship request is deleted by either the requester or recipient. + +```gdscript +func _ready(): + Talo.player_relationships.relationship_request_cancelled.connect(_on_request_cancelled) + +func _on_request_cancelled(player_alias: TaloPlayerAlias): + print("Request with %s was cancelled" % player_alias.identifier) +``` + +### relationship_confirmed + +Emitted when another player confirms your relationship request to them. + +```gdscript +func _ready(): + Talo.player_relationships.relationship_confirmed.connect(_on_relationship_confirmed) + +func _on_relationship_confirmed(player_alias: TaloPlayerAlias): + print("Now connected with %s" % player_alias.identifier) +``` + +### relationship_ended + +Emitted when a confirmed relationship is deleted by either player. + +```gdscript +func _ready(): + Talo.player_relationships.relationship_ended.connect(_on_relationship_ended) + +func _on_relationship_ended(player_alias: TaloPlayerAlias): + print("No longer connected with %s" % player_alias.identifier) +``` + +### message_received + +Emitted when a broadcast message is received from a connected player. + +```gdscript +func _ready(): + Talo.player_relationships.message_received.connect(_on_message_received) + +func _on_message_received(player_alias: TaloPlayerAlias, message: String): + print("%s sent: %s" % [player_alias.identifier, message]) + + # if the message is JSON, you can parse it + var json := JSON.new() + if json.parse(message) == OK: + print("Parsed data: %s" % json.data) +``` diff --git a/docs/godot/saves.mdx b/docs/godot/saves.mdx index dc94a0c..6dbab33 100644 --- a/docs/godot/saves.mdx +++ b/docs/godot/saves.mdx @@ -28,7 +28,7 @@ Talo's game saves let you easily save and load specific nodes in your game. To d Once a loadable has entered the tree, its `_ready()` function registers it with the saves manager. We need to register all the Loadables in the scene so that when we load our save, we can match the content in the save file with the structure of the scene. :::caution -If your loadable overrides the `_ready()` method, ensure that it contains `super()` so that Talo can still register the loadable. +If your loadable overrides the `_ready()` function, ensure that it contains `super()` so that Talo can still register the loadable. ::: Importantly, each Loadable _must_ have a unique `id` so that Talo knows which node to load with which data. This can be set in the inspector or in the code. diff --git a/docs/godot/socket.mdx b/docs/godot/socket.mdx index 601e453..3a0f8f2 100644 --- a/docs/godot/socket.mdx +++ b/docs/godot/socket.mdx @@ -13,7 +13,7 @@ You can learn more about how the socket works [here](../sockets/intro). ## Usage -Ideally you should never need to use the socket directly because individual services like players and channels should send the correct data on your behalf. However, the Talo Socket exposes a few public methods and signals for custom logic like handling errors and re-connecting to the server if the connection is closed. +Ideally you should never need to use the socket directly because individual services like players and channels should send the correct data on your behalf. However, the Talo Socket exposes a few public functions and signals for custom logic like handling errors and re-connecting to the server if the connection is closed. The socket connection is automatically established (this can disabled by setting `auto_connect_socket` to `false` in your config). When a player gets identified, they also get automatically identified with the socket server. diff --git a/docs/http/player-relationships-api.mdx b/docs/http/player-relationships-api.mdx new file mode 100644 index 0000000..060575a --- /dev/null +++ b/docs/http/player-relationships-api.mdx @@ -0,0 +1,19 @@ +import ServiceDocumentation from '@site/src/components/documentation/ServiceDocumentation' +import siteConfig from '@generated/docusaurus.config' +import { generateServiceTOC } from '@site/src/components/documentation/generateServiceTOC' + +# Player relationships API + +export const toc = [ + { value: 'Endpoints', id: 'endpoints', level: 2 }, + ...generateServiceTOC('PlayerRelationshipsAPIService', siteConfig) +] + +Talo's player relationships API is the easiest way to build friends lists, follower systems, party invites and other social features that need player-to-player connections. + +## Endpoints + + diff --git a/docs/integrations/steamworks.md b/docs/integrations/steamworks.md index 3e98324..0b00762 100644 --- a/docs/integrations/steamworks.md +++ b/docs/integrations/steamworks.md @@ -46,7 +46,7 @@ When you delete a leaderboard in Talo, we'll also delete it in Steamworks. ### Setting scores -When a player submits a score, we'll also push that score through to your Steamworks leaderboard. It'll use the `KeepBest` method since as mentioned above, Steamworks leaderboards are always in unique mode. +When a player submits a score, we'll also push that score through to your Steamworks leaderboard. It will use the `KeepBest` method since as mentioned above, Steamworks leaderboards are always in unique mode. ### Toggling score visibility diff --git a/docs/unity/channels.mdx b/docs/unity/channels.mdx index e776192..40390d2 100644 --- a/docs/unity/channels.mdx +++ b/docs/unity/channels.mdx @@ -226,7 +226,7 @@ await Talo.Channels.SetStorageProps( ); ``` -This method accepts any number of prop `(string, string)` tuples. You can set a prop value to `null` to delete it. Storage props that aren't being deleted will be upserted (updated if they exist, otherwise created). +This function accepts any number of prop `(string, string)` tuples. You can set a prop value to `null` to delete it. Storage props that aren't being deleted will be upserted (updated if they exist, otherwise created). #### Handling failures diff --git a/docs/unity/player-relationships.mdx b/docs/unity/player-relationships.mdx new file mode 100644 index 0000000..35d7382 --- /dev/null +++ b/docs/unity/player-relationships.mdx @@ -0,0 +1,309 @@ +--- +sidebar_position: 17 +description: Player relationships allow you to link multiple players together for social features like friends lists, parties and guilds. +--- + +import { ScopeBadges } from '@site/src/components/ScopeBadges' + +# Player relationships + +## Overview + +The Player Relationships API creates subscriptions between players. When a player broadcasts a message, all their subscribers receive it instantly. + +This flexible system supports both social features (friends lists with bidirectional relationships, follower systems with unidirectional relationships) and event-driven mechanics. Since broadcasts accept any data that can be converted to a string (text, JSON-encoded objects, etc.), you can build pub/sub systems where players react to each other's actions - like levelling up, unlocking achievements, or completing challenges. + +### Friends list demo + +A complete sample is available in the Unity package at `Assets/Talo Game Services/Talo/Samples/FriendsDemo`. This demo showcases: + +- Sending and accepting friend requests +- Real-time presence updates showing online friends +- Broadcasting messages between friends + +## Subscribing to players + + + +To subscribe to a player, you need to first decide what type of relationship you want to create: unidirectional or bidirectional. Unidirectional relationships create a subscription to the target player and bidirectional relationships will create a reciprocal subscription between both players. + +You also need to know the target player alias' ID. You can get this through [player presence updates](/docs/unity/player-presence), or by searching for players using [player search](/docs/unity/player-props#searching-for-players). + +Once you have both of these pieces of information, you can create the relationship: + +```csharp +var searchPage = await Talo.Players.Search("target_player_identifier"); +var targetAliasId = searchPage.players[0].alias.id; // this assumes we found at least one player + +// unidirectional relationship +await Talo.PlayerRelationships.SubscribeTo(targetAliasId, RelationshipType.Unidirectional); + +// or, bidirectional relationship +await Talo.PlayerRelationships.SubscribeTo(targetAliasId, RelationshipType.Bidirectional); +``` + +This creates an **unconfirmed** subscription (or two for bidirectional relationships). Players will need to confirm subscription requests before broadcasts can be received by the subscribers. + +## Confirming subscriptions + + + +If you know the player alias ID of the player that sent you a subscription request, you can confirm it using `Talo.PlayerRelationships.ConfirmSubscriptionFrom()`: + +```csharp +void Start() +{ + // players receive this event when they get a subscription request + Talo.PlayerRelationships.OnRelationshipRequestReceived += OnRelationshipRequestReceived; +} + +async void OnRelationshipRequestReceived(PlayerAlias playerAlias) +{ + await Talo.PlayerRelationships.ConfirmSubscriptionFrom(playerAlias.id); +} +``` + +Alternatively, you can list all pending subscription requests using `Talo.PlayerRelationships.GetSubscribers()`. + +This function allows you to filter by unconfirmed subscription requests. You can iterate over the results and confirm each subscription individually using their ID: + +```csharp +var options = new GetSubscribersOptions +{ + confirmed = ConfirmedFilter.Unconfirmed +}; +var subscribersPage = await Talo.PlayerRelationships.GetSubscribers(options); + +// loop through all unconfirmed subscriptions and confirm them +foreach (var subscriber in subscribersPage.subscriptions) +{ + await Talo.PlayerRelationships.ConfirmSubscriptionById(subscriber.id); +} +``` + +## Broadcasting messages + + + +Once relationships have been established, players can communicate with each other using broadcasts. When a player broadcasts a message, it is delivered to all of their confirmed subscribers (in a future release, Talo will support private/targeted broadcasts). + +To send a broadcast, use `Talo.PlayerRelationships.Broadcast()`: + +```csharp +// send a simple text message +Talo.PlayerRelationships.Broadcast("Hello everyone!"); + +// send structured data as JSON +var eventData = new +{ + eventType = "level_up", + level = 15, + timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() +}; +Talo.PlayerRelationships.Broadcast(JsonUtility.ToJson(eventData)); +``` + +Subscribers will receive these broadcasts via the `OnMessageReceived` event (see [Events](#events) below). + +## Unsubscribing + + + +Only subscribers can revoke subscriptions - the player being subscribed to cannot remove their subscribers. When a bidirectional subscription is revoked, the reciprocal relationship is automatically deleted. + +You can unsubscribe in two ways: + +**By player alias ID** (recommended for most cases): + +```csharp +// unsubscribe from a player +await Talo.PlayerRelationships.UnsubscribeFrom(targetAliasId); +``` + +**By subscription ID** (if you already have the subscription object): + +```csharp +// revoke a specific subscription +await Talo.PlayerRelationships.RevokeSubscription(subscription.id); +``` + +## Querying relationships + + + +### Checking subscription status + +Use `IsSubscribedTo()` to check if the current player has a subscription to another player: + +```csharp +// check if subscribed (any status) +var isSubscribed = await Talo.PlayerRelationships.IsSubscribedTo(targetAliasId, false); + +// check if subscribed AND confirmed +var isConfirmed = await Talo.PlayerRelationships.IsSubscribedTo(targetAliasId, true); +``` + +### Listing subscriptions + +Get all players that the current player is subscribed to using `GetSubscriptions()`: + +```csharp +// get all subscriptions +var page = await Talo.PlayerRelationships.GetSubscriptions(); +foreach (var subscription in page.subscriptions) +{ + Debug.Log($"Subscribed to: {subscription.subscribedTo.identifier}"); +} +``` + +You can filter the results using options: + +```csharp +var options = new GetSubscriptionsOptions +{ + confirmed = ConfirmedFilter.Confirmed, // only confirmed, unconfirmed, or any (default) + aliasId = targetAliasId, // filter by specific player alias ID + relationshipType = RelationshipTypeFilter.Bidirectional, // unidirectional, bidirectional, or any (default) + page = 0 // page number (default: 0) +}; + +var page = await Talo.PlayerRelationships.GetSubscriptions(options); +``` + +All filter properties are optional and default to showing all results. + +### Listing subscribers + +Get all players subscribed to the current player using `GetSubscribers()`: + +```csharp +// get all subscribers +var page = await Talo.PlayerRelationships.GetSubscribers(); +foreach (var subscription in page.subscriptions) +{ + Debug.Log($"Subscriber: {subscription.subscriber.identifier}"); +} +``` + +This function accepts the same filter options as `GetSubscriptions()`: + +```csharp +var options = new GetSubscribersOptions +{ + confirmed = ConfirmedFilter.Unconfirmed, // pending requests only + aliasId = subscriberAliasId, // filter by specific subscriber + relationshipType = RelationshipTypeFilter.Bidirectional, // filter by type + page = 1 // pagination +}; + +var page = await Talo.PlayerRelationships.GetSubscribers(options); +``` + +## Events + + + +The Player Relationships API emits events for real-time relationship updates. Subscribe to these events to respond to relationship changes and incoming messages: + +### OnRelationshipRequestReceived + +Emitted when another player sends the current player a relationship request. + +```csharp +void Start() +{ + Talo.PlayerRelationships.OnRelationshipRequestReceived += OnRelationshipRequestReceived; +} + +async void OnRelationshipRequestReceived(PlayerAlias playerAlias) +{ + Debug.Log($"{playerAlias.identifier} wants to connect with you"); + // optionally auto-confirm + await Talo.PlayerRelationships.ConfirmSubscriptionFrom(playerAlias.id); +} +``` + +### OnRelationshipRequestCancelled + +Emitted when an unconfirmed relationship request is deleted by either the requester or recipient. + +```csharp +void Start() +{ + Talo.PlayerRelationships.OnRelationshipRequestCancelled += OnRequestCancelled; +} + +void OnRequestCancelled(PlayerAlias playerAlias) +{ + Debug.Log($"Request with {playerAlias.identifier} was cancelled"); +} +``` + +### OnRelationshipConfirmed + +Emitted when another player confirms your relationship request to them. + +```csharp +void Start() +{ + Talo.PlayerRelationships.OnRelationshipConfirmed += OnRelationshipConfirmed; +} + +void OnRelationshipConfirmed(PlayerAlias playerAlias) +{ + Debug.Log($"Now connected with {playerAlias.identifier}"); +} +``` + +### OnRelationshipEnded + +Emitted when a confirmed relationship is deleted by either player. + +```csharp +void Start() +{ + Talo.PlayerRelationships.OnRelationshipEnded += OnRelationshipEnded; +} + +void OnRelationshipEnded(PlayerAlias playerAlias) +{ + Debug.Log($"No longer connected with {playerAlias.identifier}"); +} +``` + +### OnMessageReceived + +Emitted when a broadcast message is received from a connected player. + +```csharp +[System.Serializable] +public class LevelUpEvent +{ + public string eventType; + public int level; + public long timestamp; +} + +... + +void Start() +{ + Talo.PlayerRelationships.OnMessageReceived += OnMessageReceived; +} + +void OnMessageReceived(PlayerAlias playerAlias, string message) +{ + Debug.Log($"{playerAlias.identifier} sent: {message}"); + + // if the message is JSON, you can parse it + try + { + var data = JsonUtility.FromJson(message); + Debug.Log($"Parsed data: {data}"); + } + catch + { + // message was not JSON + } +} +``` diff --git a/docs/unity/saves.mdx b/docs/unity/saves.mdx index 5734bc9..a9e332f 100644 --- a/docs/unity/saves.mdx +++ b/docs/unity/saves.mdx @@ -19,7 +19,7 @@ Loadables are GameObjects that automatically have their data saved and loaded. T Once a loadable has entered the scene, its `OnEnable()` function registers it with the saves manager. We need to register all the Loadables in the scene so that when we load our save, we can match the content in the save file with the structure of the scene. :::caution -If your loadable overrides the `OnEnable()` method, ensure that it contains `base.OnEnable()` so that Talo can still register the loadable. +If your loadable overrides the `OnEnable()` function, ensure that it contains `base.OnEnable()` so that Talo can still register the loadable. ::: Importantly, each Loadable _must_ have a unique `Id` so that Talo knows which node to load with which data. This can be set in the inspector or in the code. diff --git a/docs/unity/socket.mdx b/docs/unity/socket.mdx index c9c6c7f..b083f6b 100644 --- a/docs/unity/socket.mdx +++ b/docs/unity/socket.mdx @@ -13,7 +13,7 @@ You can learn more about how the socket works [here](../sockets/intro). ## Usage -Ideally you should never need to use the socket directly because individual services like players and channels should send the correct data on your behalf. However, the Talo Socket exposes a few public methods and signals for custom logic like handling errors and re-connecting to the server if the connection is closed. +Ideally you should never need to use the socket directly because individual services like players and channels should send the correct data on your behalf. However, the Talo Socket exposes a few public functions and signals for custom logic like handling errors and re-connecting to the server if the connection is closed. The socket connection is automatically established (this can disabled by setting `autoConnectSocket` to `false` in your config). When a player gets identified, they also get automatically identified with the socket server.