From ea89e674513f436b490501c974ffec0d9f74a72e Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Fri, 5 Jun 2026 13:57:24 +0200 Subject: [PATCH 1/4] Content manifest caching #4435 --- .../Runtime/Content/ClientManifest.cs | 6 ++ .../Common/Runtime/Content/ClientManifest.cs | 12 ++- .../Editor/ContentService/ContentBaker.cs | 29 +++--- .../Runtime/Modules/Content/ContentService.cs | 96 +++++++++++++++++-- 4 files changed, 122 insertions(+), 21 deletions(-) diff --git a/cli/beamable.common/Runtime/Content/ClientManifest.cs b/cli/beamable.common/Runtime/Content/ClientManifest.cs index 131ee8fd48..f443480bfa 100644 --- a/cli/beamable.common/Runtime/Content/ClientManifest.cs +++ b/cli/beamable.common/Runtime/Content/ClientManifest.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Runtime.CompilerServices; using UnityEngine; +// ReSharper disable InconsistentNaming namespace Beamable.Common.Content { @@ -27,6 +28,11 @@ public class ClientManifest /// public List entries = new List(); + /// + /// The unique identifier for this manifest. + /// + public Optional uid = null; + /// /// Use a to filter the and get a new . /// This method will not mutate the current . Instead, it allocates a new one. diff --git a/client/Packages/com.beamable/Common/Runtime/Content/ClientManifest.cs b/client/Packages/com.beamable/Common/Runtime/Content/ClientManifest.cs index 98f177d1a7..16919fabd6 100644 --- a/client/Packages/com.beamable/Common/Runtime/Content/ClientManifest.cs +++ b/client/Packages/com.beamable/Common/Runtime/Content/ClientManifest.cs @@ -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 { @@ -30,6 +31,11 @@ public class ClientManifest /// public List entries = new List(); + /// + /// The unique identifier for this manifest. + /// + public Optional uid = null; + /// /// Use a to filter the and get a new . /// This method will not mutate the current . Instead, it allocates a new one. diff --git a/client/Packages/com.beamable/Editor/ContentService/ContentBaker.cs b/client/Packages/com.beamable/Editor/ContentService/ContentBaker.cs index bae37149e2..b775e3332b 100644 --- a/client/Packages/com.beamable/Editor/ContentService/ContentBaker.cs +++ b/client/Packages/com.beamable/Editor/ContentService/ContentBaker.cs @@ -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; @@ -182,18 +183,24 @@ private static bool Bake(ContentDataInfo[] contentData, return true; } - private static Promise RequestClientManifest(IBeamableRequester requester) + private static async Promise 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()}; - } - - 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()}; + } + + throw ex; + }); + manifest.uid = checksumInfo.uid; + return manifest; } } } diff --git a/client/Packages/com.beamable/Runtime/Modules/Content/ContentService.cs b/client/Packages/com.beamable/Runtime/Modules/Content/ContentService.cs index da198d9e95..4372565525 100644 --- a/client/Packages/com.beamable/Runtime/Modules/Content/ContentService.cs +++ b/client/Packages/com.beamable/Runtime/Modules/Content/ContentService.cs @@ -12,7 +12,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using UnityEngine; @@ -34,13 +33,15 @@ namespace Beamable.Content /// public class ManifestSubscription : PlatformSubscribable { - private readonly IDependencyProvider _provider; - /// /// 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. /// 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; @@ -49,6 +50,7 @@ public class ManifestSubscription : PlatformSubscribable _contentIdTable = new Dictionary(); + private readonly ClientManifest _bakedManifest; /// /// This will be removed in a future release. Please do not use. @@ -58,9 +60,13 @@ public class ManifestSubscription : PlatformSubscribable(); + _contentApi = provider.GetService(); + _manifestResolver = provider.GetService(); + _bakedManifest = bakedManifest; ManifestID = manifestID; _omitTags = omitTags; } @@ -138,7 +144,34 @@ protected override string CreateRefreshUrl(string scope) protected override Promise ExecuteRequest(IBeamableRequester requester, string url) { - return _provider.GetService().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.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) @@ -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(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"; + } } /// @@ -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 => { }); } From 373952b2a84f7c0959b58b9d3721a995e7b9dfa4 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Fri, 17 Jul 2026 16:48:54 +0200 Subject: [PATCH 2/4] Fix manifest --- .../Runtime/Modules/Content/ContentService.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/client/Packages/com.beamable/Runtime/Modules/Content/ContentService.cs b/client/Packages/com.beamable/Runtime/Modules/Content/ContentService.cs index 4372565525..29bb4a6de3 100644 --- a/client/Packages/com.beamable/Runtime/Modules/Content/ContentService.cs +++ b/client/Packages/com.beamable/Runtime/Modules/Content/ContentService.cs @@ -185,7 +185,7 @@ protected override void OnRefresh(ClientManifest data) _manifestPromise.CompleteSuccess(new Unit()); } - + private bool TryGetCachedManifest(string uid, out ClientManifest manifest) { manifest = null; @@ -227,7 +227,7 @@ private void SaveCachedManifest(string uid, ClientManifest manifest) Debug.LogError($"Failed to save cached manifest: {ex.Message}"); } } - + private string GetManifestPath(string uid) { var pid = Beam.RuntimeConfigProvider.Pid; @@ -302,7 +302,7 @@ public class ContentService : IContentApi, /// Member holds all content that has been cached as a result of fetching new content /// public ContentDataInfoWrapper CachedContentDataInfo = new ContentDataInfoWrapper(); - + /// /// Member holds all content that was baked into the current build /// To resolve content, please use the method. @@ -367,7 +367,7 @@ public ContentService(IDependencyProvider provider, // Subscribable = _provider.GetService() // .CreateSubscription(CurrentDefaultManifestID); - Subscribable = new ManifestSubscription(_provider, CurrentDefaultManifestID, _config.OmitContentManifestTags); + Subscribable = new ManifestSubscription(_provider, CurrentDefaultManifestID, _config.OmitContentManifestTags, BakedManifest); Subscribable.Subscribe(cb => { // pay attention, server... @@ -405,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 { @@ -455,7 +455,7 @@ public static string ContentPath(IBeamableFilesystemAccessor fsa) return $"{fsa.GetPersistentDataPathWithoutTrailingSlash()}/{pid}-{cid}/content/content.json"; } - + /// /// get the baked content and hold it as an in-memory dictionary for use later /// @@ -467,7 +467,7 @@ ContentDataInfoWrapper LoadBakedContent() { return new ContentDataInfoWrapper(); } - + string json = bakedFile.text; var isValidJson = Json.IsValidJson(json); if (isValidJson) @@ -611,7 +611,7 @@ public Promise GetContent(string contentId, Type contentType, st public Promise GetContent(IContentRef reference, string manifestID = "") { - + var referencedType = reflectionCache.GetTypeFromId(reference.GetId()); return GetContent(reference.GetId(), referencedType, DetermineManifestID(manifestID)); } From e83410204e0d369c65ecfbc01f901340add5d503 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Fri, 17 Jul 2026 16:52:16 +0200 Subject: [PATCH 3/4] Squashed commit of the following: commit 951e62687efdf2546da35217f33555e41997abae Author: Piotr Siuszko Date: Fri Jul 17 16:51:42 2026 +0200 Merge release/5.1.x back to main --- cli/cli/CHANGELOG.md | 7 ++++++- client/Packages/com.beamable/CHANGELOG.md | 10 +++++++++- .../Environment/Resources/versions-default.json | 2 +- microservice/microservice/CHANGELOG.md | 2 +- 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/cli/cli/CHANGELOG.md b/cli/cli/CHANGELOG.md index d0ecd425d5..9081685ec2 100644 --- a/cli/cli/CHANGELOG.md +++ b/cli/cli/CHANGELOG.md @@ -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 diff --git a/client/Packages/com.beamable/CHANGELOG.md b/client/Packages/com.beamable/CHANGELOG.md index 1e6ce8335b..0f32e6a2a6 100644 --- a/client/Packages/com.beamable/CHANGELOG.md +++ b/client/Packages/com.beamable/CHANGELOG.md @@ -11,7 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 @@ -26,6 +32,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 @@ -55,6 +62,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 diff --git a/client/Packages/com.beamable/Runtime/Environment/Resources/versions-default.json b/client/Packages/com.beamable/Runtime/Environment/Resources/versions-default.json index 83f97e2a7c..f50b7a18fe 100644 --- a/client/Packages/com.beamable/Runtime/Environment/Resources/versions-default.json +++ b/client/Packages/com.beamable/Runtime/Environment/Resources/versions-default.json @@ -1,3 +1,3 @@ { - "nugetPackageVersion": "7.2.0" + "nugetPackageVersion": "7.2.2" } diff --git a/microservice/microservice/CHANGELOG.md b/microservice/microservice/CHANGELOG.md index 9da9e84fc9..bc6bf847d3 100644 --- a/microservice/microservice/CHANGELOG.md +++ b/microservice/microservice/CHANGELOG.md @@ -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. From b753532dd0a2df3134f5d07a5dc479669072e2b5 Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Fri, 17 Jul 2026 16:53:23 +0200 Subject: [PATCH 4/4] Update changelog --- client/Packages/com.beamable/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/Packages/com.beamable/CHANGELOG.md b/client/Packages/com.beamable/CHANGELOG.md index 0f32e6a2a6..ad6de050ee 100644 --- a/client/Packages/com.beamable/CHANGELOG.md +++ b/client/Packages/com.beamable/CHANGELOG.md @@ -7,6 +7,10 @@ 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)