Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cli/beamable.common/Runtime/Content/ClientManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using System.Runtime.CompilerServices;
using UnityEngine;
// ReSharper disable InconsistentNaming

namespace Beamable.Common.Content
{
Expand All @@ -27,6 +28,11 @@ public class ClientManifest
/// </summary>
public List<ClientContentInfo> entries = new List<ClientContentInfo>();

/// <summary>
/// The unique identifier for this manifest.
/// </summary>
public Optional<string> uid = null;

/// <summary>
/// Use a <see cref="ContentQuery"/> to filter the <see cref="entries"/> and get a new <see cref="ClientManifest"/>.
/// This method will not mutate the <i>current</i> <see cref="ClientManifest"/>. Instead, it allocates a new one.
Expand Down
7 changes: 6 additions & 1 deletion cli/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed
- Portal extension scanning no longer excludes sym linked package files

## [7.2.2] - 2026-07-16

### Fixed

- Fixed an issue where SmallerJson was not serializing/deserializing null fields to default values.

## [7.2.1] - 2026-06-29
## [7.2.1] - 2026-06-30

### Changed

Expand Down
14 changes: 13 additions & 1 deletion client/Packages/com.beamable/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Content Manifest caching between runs.

### Fixed

- Fixed the Content window Snapshot tab hanging at "Loading snapshots" when snapshots in different realm folders share a name (e.g. `LastPublished-global.json` auto snapshots from publishing to multiple realms)

## [5.1.1] - 2026-06-29
## [5.1.2] - 2026-07-16

### Changed

- Update CLI to 7.2.2

## [5.1.1] - 2026-06-30

### Added

Expand All @@ -26,6 +36,7 @@ tracked error buffer.
### Changed

- Validation of the content no longer can automatically try to update the content value
- Update CLI to 7.2.1



Expand Down Expand Up @@ -55,6 +66,7 @@ tracked error buffer.
- AdminFlow prefab (ConsoleFlow) was removed from the Beam Samples Window and is now considered Deprecated
- Now Renamed content entries will be shown as Modified Renamed in the Content Manager Window rather than a New and Deleted entries.
- Default assets and content are imported manually on first visit to Content Manager
- Update CLI to 7.2.0

## [5.0.1] - 2026-04-02
### Fixed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// This file generated by a copy-operation from another project.
// Edits to this file will be overwritten by the build process.

// This file generated by a copy-operation from another project.
// Edits to this file will be overwritten by the build process.
using Beamable.Common.Api.Content;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using UnityEngine;
// ReSharper disable InconsistentNaming

namespace Beamable.Common.Content
{
Expand All @@ -30,6 +31,11 @@ public class ClientManifest
/// </summary>
public List<ClientContentInfo> entries = new List<ClientContentInfo>();

/// <summary>
/// The unique identifier for this manifest.
/// </summary>
public Optional<string> uid = null;

/// <summary>
/// Use a <see cref="ContentQuery"/> to filter the <see cref="entries"/> and get a new <see cref="ClientManifest"/>.
/// This method will not mutate the <i>current</i> <see cref="ClientManifest"/>. Instead, it allocates a new one.
Expand Down
29 changes: 18 additions & 11 deletions client/Packages/com.beamable/Editor/ContentService/ContentBaker.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Beamable;
using Beamable.Api;
using Beamable.Api.Autogenerated.Content;
using Beamable.Common;
using Beamable.Common.Api;
using Beamable.Common.BeamCli.Contracts;
Expand Down Expand Up @@ -182,18 +183,24 @@ private static bool Bake(ContentDataInfo[] contentData,
return true;
}

private static Promise<ClientManifest> RequestClientManifest(IBeamableRequester requester)
private static async Promise<ClientManifest> RequestClientManifest(IBeamableRequester requester)
{
string url = $"/basic/content/manifest/public?id={ContentConfiguration.Instance.RuntimeManifestID}";
return requester.Request(Method.GET, url, null, true, ClientManifest.ParseCSV, true).Recover(ex =>
{
if (ex is PlatformRequesterException err && err.Status == 404)
{
return new ClientManifest {entries = new List<ClientContentInfo>()};
}

throw ex;
});
string id = ContentConfiguration.Instance.RuntimeManifestID;
var api = new ContentApi(requester);
var checksumInfo = await api.GetManifestChecksum(id);
string url = $"/basic/content/manifest/public?uid={checksumInfo.uid}";
var manifest = await requester.Request(Method.GET, url, null, true, ClientManifest.ParseCSV, true)
.Recover(ex =>
{
if (ex is PlatformRequesterException err && err.Status == 404)
{
return new ClientManifest {entries = new List<ClientContentInfo>()};
}

throw ex;
});
manifest.uid = checksumInfo.uid;
return manifest;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"nugetPackageVersion": "7.2.0"
"nugetPackageVersion": "7.2.2"
}
112 changes: 97 additions & 15 deletions client/Packages/com.beamable/Runtime/Modules/Content/ContentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using UnityEngine;
Expand All @@ -34,13 +33,15 @@ namespace Beamable.Content
/// </summary>
public class ManifestSubscription : PlatformSubscribable<ClientManifest, ClientManifest>
{
private readonly IDependencyProvider _provider;

/// <summary>
/// Every content manifest has an ID. Usually, a game will only have a single content manifest called "global", but it is possible to add more.
/// </summary>
public string ManifestID { get; } = "global";

private readonly IBeamableFilesystemAccessor _filesystemAccessor;
private readonly IManifestResolver _manifestResolver;
private readonly Api.Autogenerated.Content.IContentApi _contentApi;

private readonly bool _omitTags;

private ClientManifest _latestManifiest;
Expand All @@ -49,6 +50,7 @@ public class ManifestSubscription : PlatformSubscribable<ClientManifest, ClientM

private readonly Dictionary<string, ClientContentInfo> _contentIdTable =
new Dictionary<string, ClientContentInfo>();
private readonly ClientManifest _bakedManifest;

/// <summary>
/// This will be removed in a future release. Please do not use.
Expand All @@ -58,9 +60,13 @@ public class ManifestSubscription : PlatformSubscribable<ClientManifest, ClientM

public ManifestSubscription(IDependencyProvider provider,
string manifestID,
bool omitTags = false) : base(provider, "content")
bool omitTags = false,
ClientManifest bakedManifest = null) : base(provider, "content")
{
_provider = provider;
_filesystemAccessor = provider.GetService<IBeamableFilesystemAccessor>();
_contentApi = provider.GetService<Api.Autogenerated.Content.IContentApi>();
_manifestResolver = provider.GetService<IManifestResolver>();
_bakedManifest = bakedManifest;
ManifestID = manifestID;
_omitTags = omitTags;
}
Expand Down Expand Up @@ -138,7 +144,34 @@ protected override string CreateRefreshUrl(string scope)

protected override Promise<ClientManifest> ExecuteRequest(IBeamableRequester requester, string url)
{
return _provider.GetService<IManifestResolver>().ResolveManifest(requester, url, this);
// There could be some use cases for game makers where they would want to opt out of manifest caching.
// Define symbol `BEAMABLE_OPTOUT_MANIFEST_FILE_CACHING` allows them to bypass manifest filesystem caching.
#if BEAMABLE_OPTOUT_MANIFEST_FILE_CACHING
return _manifestResolver.ResolveManifest(requester, url, this);
#else
return _contentApi.GetManifestChecksum(ManifestID).FlatMap(checksumResponse =>
{
if (!checksumResponse.uid.HasValue)
{
return _manifestResolver.ResolveManifest(requester, url, this);
}

string uid = checksumResponse.uid.Value;
if (TryGetCachedManifest(uid, out var cachedManifest))
{
return Promise<ClientManifest>.Successful(cachedManifest);
}

string downloadUrl = url + $"&uid={uid}";
return _manifestResolver
.ResolveManifest(requester, downloadUrl, this)
.Map(manifest =>
{
SaveCachedManifest(uid, manifest);
return manifest;
});
});
#endif
}

protected override void OnRefresh(ClientManifest data)
Expand All @@ -152,6 +185,55 @@ protected override void OnRefresh(ClientManifest data)

_manifestPromise.CompleteSuccess(new Unit());
}

private bool TryGetCachedManifest(string uid, out ClientManifest manifest)
{
manifest = null;
try
{
if (_bakedManifest != null && _bakedManifest.uid.TryGet(out var bakedUid) && uid.Equals(bakedUid))
{
manifest = _bakedManifest;
return manifest != null;
}
string path = GetManifestPath(uid);
if (!File.Exists(path)) return false;

string json = File.ReadAllText(path);
manifest = JsonUtility.FromJson<ClientManifest>(json);
manifest.uid = uid;
return manifest != null;
}
catch (Exception ex)
{
Debug.LogWarning($"Failed to load cached manifest: {ex.Message}");
return false;
}
}

private void SaveCachedManifest(string uid, ClientManifest manifest)
{
try
{
string path = GetManifestPath(uid);
string dir = Path.GetDirectoryName(path);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);

string json = JsonUtility.ToJson(manifest);
File.WriteAllText(path, json);
}
catch (Exception ex)
{
Debug.LogError($"Failed to save cached manifest: {ex.Message}");
}
}

private string GetManifestPath(string uid)
{
var pid = Beam.RuntimeConfigProvider.Pid;
var cid = Beam.RuntimeConfigProvider.Cid;
return $"{_filesystemAccessor.GetPersistentDataPathWithoutTrailingSlash()}/{pid}-{cid}/content/manifests/{ManifestID}_{uid}.json";
}
}

/// <summary>
Expand Down Expand Up @@ -220,7 +302,7 @@ public class ContentService : IContentApi,
/// Member holds all content that has been cached as a result of fetching new content
/// </summary>
public ContentDataInfoWrapper CachedContentDataInfo = new ContentDataInfoWrapper();

/// <summary>
/// Member holds all content that was baked into the current build
/// To resolve content, please use the <see cref="GetContent(string,string)"/> method.
Expand Down Expand Up @@ -285,7 +367,7 @@ public ContentService(IDependencyProvider provider,

// Subscribable = _provider.GetService<IManifestSubscriptionFactory>()
// .CreateSubscription(CurrentDefaultManifestID);
Subscribable = new ManifestSubscription(_provider, CurrentDefaultManifestID, _config.OmitContentManifestTags);
Subscribable = new ManifestSubscription(_provider, CurrentDefaultManifestID, _config.OmitContentManifestTags, BakedManifest);
Subscribable.Subscribe(cb =>
{
// pay attention, server...
Expand Down Expand Up @@ -323,14 +405,14 @@ private IEnumerator WatchCacheAndWriteToDisk()
yield return delay;
if (cacheVersion != CachedContentDataInfo.cacheVersion)
{
// there has been a cache update since we waited. We should wait
// there has been a cache update since we waited. We should wait
// some more time to let all the changes roll in.
continue;
}

// check if cache needs to be saved
if (CachedContentDataInfo.cacheVersion <= committedCacheVersion)
continue; // wait for another 5 seconds ?
continue; // wait for another 5 seconds ?

try
{
Expand Down Expand Up @@ -373,7 +455,7 @@ public static string ContentPath(IBeamableFilesystemAccessor fsa)

return $"{fsa.GetPersistentDataPathWithoutTrailingSlash()}/{pid}-{cid}/content/content.json";
}

/// <summary>
/// get the baked content and hold it as an in-memory dictionary for use later
/// </summary>
Expand All @@ -385,7 +467,7 @@ ContentDataInfoWrapper LoadBakedContent()
{
return new ContentDataInfoWrapper();
}

string json = bakedFile.text;
var isValidJson = Json.IsValidJson(json);
if (isValidJson)
Expand Down Expand Up @@ -469,7 +551,7 @@ private void AddSubscriber(string manifestID)
if (Subscribables.ContainsKey(manifestID))
return;

Subscribables.Add(manifestID, new ManifestSubscription(_provider, manifestID, _config.OmitContentManifestTags));
Subscribables.Add(manifestID, new ManifestSubscription(_provider, manifestID, _config.OmitContentManifestTags, BakedManifest));
Subscribables[manifestID].Subscribe(cb => { });
}

Expand Down Expand Up @@ -529,7 +611,7 @@ public Promise<IContentObject> GetContent(string contentId, Type contentType, st

public Promise<IContentObject> GetContent(IContentRef reference, string manifestID = "")
{

var referencedType = reflectionCache.GetTypeFromId(reference.GetId());
return GetContent(reference.GetId(), referencedType, DetermineManifestID(manifestID));
}
Expand Down
2 changes: 1 addition & 1 deletion microservice/microservice/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [7.2.1] - Undetermined
## [7.2.1] - 2026-06-30

### Fixed
- Microservice content fetches now retry with bounded exponential backoff and jitter. This applies to both manifest and content entry fetches.
Expand Down
Loading