From ea89e674513f436b490501c974ffec0d9f74a72e Mon Sep 17 00:00:00 2001 From: Piotr Siuszko Date: Fri, 5 Jun 2026 13:57:24 +0200 Subject: [PATCH] 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 => { }); }