0.3.0: UGC removed, battlepass context-carrying API, scenario behavior aligned with web SDK, CI publish
This commit is contained in:
@@ -6,16 +6,17 @@ using RudderSdk.Core.Models.BattlePass;
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Battle pass progress and rewards. The simple overloads target the project's
|
||||
/// default battle pass; the request-based overloads carry the scenario node
|
||||
/// context and are used by the scenario runtime.
|
||||
/// Battle pass progress and rewards. Battle pass state is tied to a scenario
|
||||
/// battle pass node, so every call carries the scenario/node ids (and a run id
|
||||
/// for the mutating calls) — <see cref="BattlePassSession"/> supplies them
|
||||
/// during scenario runs.
|
||||
/// </summary>
|
||||
public sealed class BattlePassService
|
||||
{
|
||||
/// <summary>Free reward track, see <see cref="ClaimRewardAsync(int, string, CancellationToken)"/>.</summary>
|
||||
/// <summary>Free reward track, see <see cref="ClaimRewardAsync(ClaimBattlePassRewardRequest, CancellationToken)"/>.</summary>
|
||||
public const string TrackFree = "free";
|
||||
|
||||
/// <summary>Premium reward track, see <see cref="ClaimRewardAsync(int, string, CancellationToken)"/>.</summary>
|
||||
/// <summary>Premium reward track, see <see cref="ClaimRewardAsync(ClaimBattlePassRewardRequest, CancellationToken)"/>.</summary>
|
||||
public const string TrackPremium = "premium";
|
||||
|
||||
private readonly RudderClient _client;
|
||||
@@ -23,55 +24,43 @@ public sealed class BattlePassService
|
||||
internal BattlePassService(RudderClient client) => _client = client;
|
||||
|
||||
/// <summary>Reads current progress: xp, level, premium ownership, claimed tiers.</summary>
|
||||
public Task<GetBattlePassProgressResponse> GetProgressAsync(CancellationToken cancellationToken = default)
|
||||
=> GetProgressAsync(new GetBattlePassProgressRequest(), cancellationToken);
|
||||
|
||||
/// <summary>Credits xp and returns the new xp/level and level-up flags.</summary>
|
||||
public Task<AddBattlePassXpResponse> AddXpAsync(long amount, CancellationToken cancellationToken = default)
|
||||
=> AddXpAsync(new AddBattlePassXpRequest { Amount = amount }, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Claims a tier reward at a reached level (idempotent server-side).
|
||||
/// <paramref name="track"/> is <see cref="TrackFree"/> or <see cref="TrackPremium"/>.
|
||||
/// </summary>
|
||||
public Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken cancellationToken = default)
|
||||
=> ClaimRewardAsync(new ClaimBattlePassRewardRequest { Level = level, Track = track }, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Purchases the premium track (charges the wallet). When
|
||||
/// <paramref name="idempotencyKey"/> is null, a random one is generated.
|
||||
/// </summary>
|
||||
public Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(string? idempotencyKey = null, CancellationToken cancellationToken = default)
|
||||
=> PurchasePremiumAsync(new PurchaseBattlePassPremiumRequest
|
||||
{
|
||||
IdempotencyKey = idempotencyKey ?? Guid.NewGuid().ToString()
|
||||
}, cancellationToken);
|
||||
|
||||
internal Task<GetBattlePassProgressResponse> GetProgressAsync(GetBattlePassProgressRequest request, CancellationToken cancellationToken = default)
|
||||
public Task<GetBattlePassProgressResponse> GetProgressAsync(string scenarioId, string nodeId, CancellationToken cancellationToken = default)
|
||||
=> _client.SendAsync<GetBattlePassProgressRequest, GetBattlePassProgressResponse>(
|
||||
"POST",
|
||||
"/sdk/v1/battlepass/progress",
|
||||
request,
|
||||
new GetBattlePassProgressRequest { ScenarioId = scenarioId, NodeId = nodeId },
|
||||
cancellationToken);
|
||||
|
||||
internal Task<AddBattlePassXpResponse> AddXpAsync(AddBattlePassXpRequest request, CancellationToken cancellationToken = default)
|
||||
/// <summary>Credits xp and returns the new xp/level and level-up flags.</summary>
|
||||
public Task<AddBattlePassXpResponse> AddXpAsync(AddBattlePassXpRequest request, CancellationToken cancellationToken = default)
|
||||
=> _client.SendAsync<AddBattlePassXpRequest, AddBattlePassXpResponse>(
|
||||
"POST",
|
||||
"/sdk/v1/battlepass/xp",
|
||||
request,
|
||||
cancellationToken);
|
||||
|
||||
internal Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(ClaimBattlePassRewardRequest request, CancellationToken cancellationToken = default)
|
||||
/// <summary>
|
||||
/// Claims a tier reward at a reached level (idempotent server-side).
|
||||
/// <see cref="ClaimBattlePassRewardRequest.Track"/> is <see cref="TrackFree"/> or <see cref="TrackPremium"/>.
|
||||
/// </summary>
|
||||
public Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(ClaimBattlePassRewardRequest request, CancellationToken cancellationToken = default)
|
||||
=> _client.SendAsync<ClaimBattlePassRewardRequest, ClaimBattlePassRewardResponse>(
|
||||
"POST",
|
||||
"/sdk/v1/battlepass/claim",
|
||||
request,
|
||||
cancellationToken);
|
||||
|
||||
internal Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(PurchaseBattlePassPremiumRequest request, CancellationToken cancellationToken = default)
|
||||
=> _client.SendAsync<PurchaseBattlePassPremiumRequest, PurchaseBattlePassPremiumResponse>(
|
||||
/// <summary>
|
||||
/// Purchases the premium track (charges the wallet). When
|
||||
/// <see cref="PurchaseBattlePassPremiumRequest.IdempotencyKey"/> is null, a random one is generated.
|
||||
/// </summary>
|
||||
public Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(PurchaseBattlePassPremiumRequest request, CancellationToken cancellationToken = default)
|
||||
{
|
||||
request.IdempotencyKey ??= Guid.NewGuid().ToString();
|
||||
return _client.SendAsync<PurchaseBattlePassPremiumRequest, PurchaseBattlePassPremiumResponse>(
|
||||
"POST",
|
||||
"/sdk/v1/battlepass/premium",
|
||||
request,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,12 +227,16 @@ public sealed partial class ScenarioService
|
||||
},
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response?.Plan != null)
|
||||
StartPlan(response.Plan);
|
||||
// The server reports objective completion; it no longer returns a plan from the
|
||||
// counter endpoint. On completion, cross the node's onComplete handle (which
|
||||
// advances the run) — idempotent if the consumer also completes the session.
|
||||
if (response != null && response.Completed)
|
||||
await CompleteNodeAsync(runId, nodeId, "onComplete", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
FailRun(run, nodeId, ex);
|
||||
// Counter update failure does not fail the run.
|
||||
_client.Options.Logger?.Log(RudderLogLevel.Warning, $"[Rudder] Scenario counter update failed at node {nodeId}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,10 @@ public sealed partial class ScenarioService
|
||||
EmitBattlePassLevel(context);
|
||||
break;
|
||||
default:
|
||||
// Unsupported node type — fail the run (surfaced via OnScenarioFailed)
|
||||
// instead of leaving it stalled on a node no handler will complete.
|
||||
_client.Options.Logger?.Log(RudderLogLevel.Warning, $"[Rudder] Unsupported scenario node type '{node.Type}' ({node.Id}).");
|
||||
FailRun(run, state.NodeId, new Exception($"Unsupported scenario node type '{node.Type}'"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -124,7 +127,7 @@ public sealed partial class ScenarioService
|
||||
|
||||
private void EmitBattlePass(ScenarioNodeContext context)
|
||||
{
|
||||
OnBattlePass?.Invoke(new BattlePassSession(context));
|
||||
OnBattlePass?.Invoke(new BattlePassSession(context, _client.BattlePass));
|
||||
}
|
||||
|
||||
private void EmitBattlePassLevel(ScenarioNodeContext context)
|
||||
|
||||
@@ -3,7 +3,11 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>Session of a scenario battle-pass-level node.</summary>
|
||||
/// <summary>
|
||||
/// Session of a scenario battle-pass-level node — a single claimable tier.
|
||||
/// <see cref="ClaimAsync"/> crosses onComplete, which the server accepts only
|
||||
/// once the player has reached the node's configured level.
|
||||
/// </summary>
|
||||
public sealed class BattlePassLevelSession
|
||||
{
|
||||
internal BattlePassLevelSession(ScenarioNodeContext context) => Context = context;
|
||||
@@ -14,12 +18,15 @@ public sealed class BattlePassLevelSession
|
||||
/// <summary>Node id.</summary>
|
||||
public string Id => Context.NodeId;
|
||||
|
||||
/// <summary>The tier level this node claims.</summary>
|
||||
public int Level => Context.Get("levelNumber", 0);
|
||||
|
||||
/// <summary>Reads a typed value from the node data.</summary>
|
||||
public T Get<T>(string key, T defaultValue = default!) => Context.Get(key, defaultValue);
|
||||
|
||||
/// <summary>Advances the run through the onComplete handle.</summary>
|
||||
public Task CompleteAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onComplete", cancellationToken);
|
||||
/// <summary>Claims this tier; crosses onComplete (the server checks the level was reached).</summary>
|
||||
public Task ClaimAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onComplete", cancellationToken);
|
||||
|
||||
/// <summary>Advances the run through the onComplete handle (fire-and-forget).</summary>
|
||||
public void Complete() => Context.Complete("onComplete");
|
||||
/// <summary>Claims this tier (fire-and-forget).</summary>
|
||||
public void Claim() => Context.Complete("onComplete");
|
||||
}
|
||||
|
||||
@@ -1,12 +1,24 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using RudderSdk.Core.Models.BattlePass;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>Session of a scenario battle-pass node.</summary>
|
||||
/// <summary>
|
||||
/// Session of a scenario battle-pass node. Exposes the battle pass operations
|
||||
/// bound to this node's scenario/node/run ids, plus explicit boundary crossings
|
||||
/// the game drives from its UI; the server validates each crossing.
|
||||
/// </summary>
|
||||
public sealed class BattlePassSession
|
||||
{
|
||||
internal BattlePassSession(ScenarioNodeContext context) => Context = context;
|
||||
private readonly BattlePassService _battlePass;
|
||||
|
||||
internal BattlePassSession(ScenarioNodeContext context, BattlePassService battlePass)
|
||||
{
|
||||
Context = context;
|
||||
_battlePass = battlePass;
|
||||
}
|
||||
|
||||
/// <summary>Underlying node context.</summary>
|
||||
public ScenarioNodeContext Context { get; }
|
||||
@@ -17,6 +29,52 @@ public sealed class BattlePassSession
|
||||
/// <summary>Reads a typed value from the node data.</summary>
|
||||
public T Get<T>(string key, T defaultValue = default!) => Context.Get(key, defaultValue);
|
||||
|
||||
/// <summary>Reads current progress (xp, level, premium ownership, claimed tiers) for this node.</summary>
|
||||
public Task<GetBattlePassProgressResponse> GetProgressAsync(CancellationToken cancellationToken = default)
|
||||
=> _battlePass.GetProgressAsync(Context.ScenarioId, Context.NodeId, cancellationToken);
|
||||
|
||||
/// <summary>Credits xp from a configured source.</summary>
|
||||
public Task<AddBattlePassXpResponse> AddXpAsync(string source, long amount, CancellationToken cancellationToken = default)
|
||||
=> _battlePass.AddXpAsync(new AddBattlePassXpRequest
|
||||
{
|
||||
ScenarioId = Context.ScenarioId,
|
||||
NodeId = Context.NodeId,
|
||||
RunId = Context.RunId,
|
||||
Source = source,
|
||||
Amount = amount
|
||||
}, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Claims a tier reward at a reached level.
|
||||
/// <paramref name="track"/> is <see cref="BattlePassService.TrackFree"/> or <see cref="BattlePassService.TrackPremium"/>.
|
||||
/// </summary>
|
||||
public Task<ClaimBattlePassRewardResponse> ClaimRewardAsync(int level, string track, CancellationToken cancellationToken = default)
|
||||
=> _battlePass.ClaimRewardAsync(new ClaimBattlePassRewardRequest
|
||||
{
|
||||
ScenarioId = Context.ScenarioId,
|
||||
NodeId = Context.NodeId,
|
||||
RunId = Context.RunId,
|
||||
Level = level,
|
||||
Track = track
|
||||
}, cancellationToken);
|
||||
|
||||
/// <summary>Purchases the premium track, then crosses onPremiumPurchase on success.</summary>
|
||||
public async Task<PurchaseBattlePassPremiumResponse> PurchasePremiumAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _battlePass.PurchasePremiumAsync(new PurchaseBattlePassPremiumRequest
|
||||
{
|
||||
ScenarioId = Context.ScenarioId,
|
||||
NodeId = Context.NodeId,
|
||||
RunId = Context.RunId,
|
||||
IdempotencyKey = Guid.NewGuid().ToString()
|
||||
}, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response != null && response.Success)
|
||||
await Context.CompleteAsync("onPremiumPurchase", cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response!;
|
||||
}
|
||||
|
||||
/// <summary>Advances the run through the onLevelUp handle.</summary>
|
||||
public Task LevelUpAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onLevelUp", cancellationToken);
|
||||
|
||||
@@ -29,12 +87,6 @@ public sealed class BattlePassSession
|
||||
/// <summary>Advances the run through the onMaxLevel handle (fire-and-forget).</summary>
|
||||
public void MaxLevel() => Context.Complete("onMaxLevel");
|
||||
|
||||
/// <summary>Advances the run through the onPremiumPurchase handle.</summary>
|
||||
public Task PremiumPurchaseAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onPremiumPurchase", cancellationToken);
|
||||
|
||||
/// <summary>Advances the run through the onPremiumPurchase handle (fire-and-forget).</summary>
|
||||
public void PremiumPurchase() => Context.Complete("onPremiumPurchase");
|
||||
|
||||
/// <summary>Advances the run through the onComplete handle.</summary>
|
||||
public Task CompleteAsync(CancellationToken cancellationToken = default) => Context.CompleteAsync("onComplete", cancellationToken);
|
||||
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using RudderSdk.Core.Models.Ugc;
|
||||
|
||||
namespace RudderSdk.Core;
|
||||
|
||||
/// <summary>User-generated content: upload, list, download URLs, deletion.</summary>
|
||||
public sealed class UgcService
|
||||
{
|
||||
private readonly RudderClient _client;
|
||||
|
||||
internal UgcService(RudderClient client) => _client = client;
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a file in one call: pre-signed URL, PUT of the bytes, submission.
|
||||
/// Requires an upload transport in <see cref="RudderClientOptions.UploadTransport"/>.
|
||||
/// </summary>
|
||||
public async Task<UgcSubmission> UploadAsync(
|
||||
string name,
|
||||
byte[] data,
|
||||
string? description = null,
|
||||
JToken? metadata = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var uploadTransport = _client.Options.UploadTransport
|
||||
?? throw new InvalidOperationException("UploadTransport is not configured.");
|
||||
|
||||
var upload = await GetUploadUrlAsync(name, cancellationToken).ConfigureAwait(false);
|
||||
await uploadTransport.PutAsync(
|
||||
upload.UploadUrl,
|
||||
data ?? Array.Empty<byte>(),
|
||||
GetContentType(name),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return await SubmitAsync(
|
||||
name,
|
||||
upload.FileKey,
|
||||
data?.LongLength ?? 0,
|
||||
description,
|
||||
metadata,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>Fetches one page of the player's submissions.</summary>
|
||||
public Task<ListUgcResponse> ListAsync(string? status = null, int limit = 20, string? cursor = null, CancellationToken cancellationToken = default)
|
||||
=> _client.SendAsync<ListUgcResponse>(
|
||||
"GET",
|
||||
"/sdk/v1/ugc" + Url.Query(
|
||||
("status", status),
|
||||
("limit", limit > 0 ? limit.ToString() : null),
|
||||
("cursor", cursor)),
|
||||
cancellationToken);
|
||||
|
||||
/// <summary>Iterates over all of the player's submissions, following the cursor pagination.</summary>
|
||||
public async IAsyncEnumerable<UgcSubmission> ListAllAsync(
|
||||
string? status = null,
|
||||
int limit = 20,
|
||||
[EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
string? cursor = null;
|
||||
do
|
||||
{
|
||||
var page = await ListAsync(status, limit, cursor, cancellationToken).ConfigureAwait(false);
|
||||
if (page?.Items != null)
|
||||
{
|
||||
foreach (var item in page.Items)
|
||||
yield return item;
|
||||
}
|
||||
|
||||
cursor = page?.NextCursor;
|
||||
}
|
||||
while (!string.IsNullOrEmpty(cursor));
|
||||
}
|
||||
|
||||
/// <summary>Returns one submission.</summary>
|
||||
public Task<UgcSubmission> GetAsync(string id, CancellationToken cancellationToken = default)
|
||||
=> _client.SendAsync<UgcSubmission>("GET", "/sdk/v1/ugc/" + Url.Encode(id), cancellationToken);
|
||||
|
||||
/// <summary>Deletes a submission; returns false when the server refused.</summary>
|
||||
public async Task<bool> DeleteAsync(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _client.SendAsync<DeleteUgcResponse>(
|
||||
"DELETE",
|
||||
"/sdk/v1/ugc/" + Url.Encode(id),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return response == null || response.Success;
|
||||
}
|
||||
|
||||
/// <summary>Requests a pre-signed upload URL.</summary>
|
||||
public Task<GetUploadUrlResponse> GetUploadUrlAsync(string filename, CancellationToken cancellationToken = default)
|
||||
=> _client.SendAsync<GetUploadUrlResponse>(
|
||||
"GET",
|
||||
"/sdk/v1/ugc/upload-url" + Url.Query(("filename", filename)),
|
||||
cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the download URL of a submission.
|
||||
/// Throws <see cref="InvalidOperationException"/> when the server returned none.
|
||||
/// </summary>
|
||||
public async Task<string> GetDownloadUrlAsync(string id, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _client.SendAsync<GetDownloadUrlResponse>(
|
||||
"GET",
|
||||
"/sdk/v1/ugc/" + Url.Encode(id) + "/download",
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response == null || string.IsNullOrEmpty(response.DownloadUrl))
|
||||
throw new InvalidOperationException("The server returned no download URL.");
|
||||
|
||||
return response.DownloadUrl;
|
||||
}
|
||||
|
||||
/// <summary>Registers an uploaded file as a submission.</summary>
|
||||
public Task<UgcSubmission> SubmitAsync(
|
||||
string name,
|
||||
string fileKey,
|
||||
long fileSize,
|
||||
string? description = null,
|
||||
JToken? metadata = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _client.SendAsync<SubmitUgcRequest, UgcSubmission>(
|
||||
"POST",
|
||||
"/sdk/v1/ugc",
|
||||
new SubmitUgcRequest
|
||||
{
|
||||
Name = name,
|
||||
FileKey = fileKey,
|
||||
FileSize = fileSize,
|
||||
Description = description!,
|
||||
Metadata = metadata!
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private static string GetContentType(string filename)
|
||||
{
|
||||
var ext = System.IO.Path.GetExtension(filename)?.ToLowerInvariant();
|
||||
switch (ext)
|
||||
{
|
||||
case ".png": return "image/png";
|
||||
case ".jpg":
|
||||
case ".jpeg": return "image/jpeg";
|
||||
case ".gif": return "image/gif";
|
||||
case ".webp": return "image/webp";
|
||||
case ".txt": return "text/plain";
|
||||
default: return "application/octet-stream";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user