From 3880741ccdf2e77a54ab7da51241ac05d399dcbb Mon Sep 17 00:00:00 2001 From: Esteban Ordano Date: Tue, 15 Sep 2026 21:36:03 +0200 Subject: [PATCH 01/14] feat: font_src on TextShape, UiText, UiInput and UiDropdown Implements the optional font_src of decentraland/protocol#487 without an external service: a scene content file, a URL or a Google Fonts family (via Fontsource) is downloaded and built into TextMeshPro and UI Toolkit font assets at runtime; a source that fails keeps the built-in font. The bindings are the pinned @dcl/protocol plus the four font_src lines. --- .../AssetGroups/UI.asset | 5 + .../ECS/TestSuite/ECS.TestSuite.asmdef | 3 +- .../Infrastructure/ECS/TestSuite/TestFonts.cs | 23 ++ .../ECS/TestSuite/TestFonts.cs.meta | 11 + .../Components/CommonLoadingArguments.cs | 7 + .../AssetsDeferredLoadingSystem.cs | 2 + .../Tests/DeferredLoadingSystemShould.cs | 23 ++ .../ECS/Unity/StreamableLoading/Fonts.meta | 8 + .../Unity/StreamableLoading/Fonts/FontData.cs | 20 ++ .../StreamableLoading/Fonts/FontData.cs.meta | 2 + .../Fonts/FontFamilyAssets.cs | 43 ++++ .../Fonts/FontFamilyAssets.cs.meta | 2 + .../StreamableLoading/Fonts/FontFileStore.cs | 107 +++++++++ .../Fonts/FontFileStore.cs.meta | 2 + .../Fonts/FontLoadException.cs | 10 + .../Fonts/FontLoadException.cs.meta | 2 + .../StreamableLoading/Fonts/FontSourceKind.cs | 9 + .../Fonts/FontSourceKind.cs.meta | 11 + .../Fonts/FontSrcResolver.cs | 59 +++++ .../Fonts/FontSrcResolver.cs.meta | 2 + .../StreamableLoading/Fonts/FontVariant.cs | 10 + .../Fonts/FontVariant.cs.meta | 11 + .../StreamableLoading/Fonts/FontsCache.cs | 11 + .../Fonts/FontsCache.cs.meta | 2 + .../Fonts/FontsourceCatalog.cs | 103 +++++++++ .../Fonts/FontsourceCatalog.cs.meta | 2 + .../Fonts/FontsourceFamilyRecord.cs | 30 +++ .../Fonts/FontsourceFamilyRecord.cs.meta | 11 + .../Fonts/GetFontIntention.cs | 37 +++ .../Fonts/GetFontIntention.cs.meta | 2 + .../StreamableLoading/Fonts/LoadFontSystem.cs | 98 ++++++++ .../Fonts/LoadFontSystem.cs.meta | 2 + .../Fonts/RuntimeFontAssetFactory.cs | 131 +++++++++++ .../Fonts/RuntimeFontAssetFactory.cs.meta | 2 + .../Fonts/SceneFontRequest.cs | 66 ++++++ .../Fonts/SceneFontRequest.cs.meta | 2 + .../Unity/StreamableLoading/Fonts/Tests.meta | 8 + .../Fonts/Tests/FontFileStoreShould.cs | 121 ++++++++++ .../Fonts/Tests/FontFileStoreShould.cs.meta | 2 + .../Fonts/Tests/FontSrcResolverShould.cs | 101 +++++++++ .../Fonts/Tests/FontSrcResolverShould.cs.meta | 2 + .../Fonts/Tests/Fonts.Tests.asmref | 3 + .../Fonts/Tests/Fonts.Tests.asmref.meta | 7 + .../Fonts/Tests/FontsourceCatalogShould.cs | 140 ++++++++++++ .../Tests/FontsourceCatalogShould.cs.meta | 2 + .../Fonts/Tests/PlayMode.meta | 8 + .../PlayMode/Fonts.PlayMode.Tests.asmref | 3 + .../PlayMode/Fonts.PlayMode.Tests.asmref.meta | 7 + .../Tests/PlayMode/LoadFontSystemShould.cs | 70 ++++++ .../PlayMode/LoadFontSystemShould.cs.meta | 2 + .../Tests/RuntimeFontAssetFactoryShould.cs | 114 ++++++++++ .../RuntimeFontAssetFactoryShould.cs.meta | 11 + .../Fonts/Tests/SceneFontRequestShould.cs | 212 ++++++++++++++++++ .../Tests/SceneFontRequestShould.cs.meta | 11 + .../Infrastructure/Global/StaticContainer.cs | 1 + .../Integration Tests Container.asset | 10 + .../ReportsHandling/ReportCategory.cs | 5 + .../ReportsHandlingSettingsDevelopment.asset | 8 + .../ReportsHandlingSettingsProduction.asset | 8 + .../Profiling/ProfilingCounters.cs | 13 ++ .../PluginSystem/Global/Plugin Settings.asset | 10 + .../PluginSystem/World/FontsLoadingPlugin.cs | 62 +++++ .../World/FontsLoadingPlugin.cs.meta | 11 + .../DCL/PluginSystem/World/SceneUIPlugin.cs | 12 +- .../DCL/PluginSystem/World/TextShapePlugin.cs | 5 +- .../DCL/ResourcesUnloading/CacheCleaner.cs | 7 + .../SceneUI/Components/UIDropdownComponent.cs | 10 +- .../SceneUI/Components/UIInputComponent.cs | 8 + .../SceneUI/Components/UITextComponent.cs | 6 + .../UIDropdownInstantiationSystem.cs | 27 ++- .../UIDropdown/UIDropdownReleaseSystem.cs | 24 +- .../UIInput/UIInputInstantiationSystem.cs | 26 ++- .../Systems/UIInput/UIInputReleaseSystem.cs | 24 +- .../UIText/UITextInstantiationSystem.cs | 30 ++- .../Systems/UIText/UITextReleaseSystem.cs | 45 +++- .../UIDropdownInstantiationSystemShould.cs | 19 +- .../Tests/UIDropdownReleaseSystemShould.cs | 98 ++++++++ .../UIDropdownReleaseSystemShould.cs.meta | 11 + .../Tests/UIInputInstantiationSystemShould.cs | 19 +- .../Tests/UIInputReleaseSystemShould.cs | 99 ++++++++ .../Tests/UIInputReleaseSystemShould.cs.meta | 11 + .../Tests/UITextInstantiationSystemShould.cs | 68 +++++- .../Tests/UITextReleaseSystemShould.cs | 107 +++++++++ .../Tests/UITextReleaseSystemShould.cs.meta | 11 + .../SceneUI/Utils/UiElementUtils.cs | 33 ++- .../TextShape/Component/TextShapeComponent.cs | 7 + .../TextShape/Demo/TextShapeDemoWorld.cs | 5 +- .../System/InstantiateTextShapeSystem.cs | 10 +- .../System/ReleaseTextShapeSystem.cs | 80 +++++++ .../System/ReleaseTextShapeSystem.cs.meta | 2 + .../TextShape/System/UpdateTextShapeSystem.cs | 30 ++- .../TextShape/TMPProSdkExtensions.cs | 25 ++- .../DCL/SDKComponents/TextShape/Tests.meta | 8 + .../Tests/ReleaseTextShapeSystemShould.cs | 114 ++++++++++ .../ReleaseTextShapeSystemShould.cs.meta | 11 + .../TextShape/Tests/TextShape.Tests.asmref | 3 + .../Tests/TextShape.Tests.asmref.meta | 7 + .../DecentralandProtocol/TextShape.gen.cs | 77 ++++++- .../DecentralandProtocol/UiDropdown.gen.cs | 65 +++++- .../DecentralandProtocol/UiInput.gen.cs | 65 +++++- .../DecentralandProtocol/UiText.gen.cs | 66 +++++- .../ProjectSettings/GraphicsSettings.asset | 1 + 102 files changed, 3028 insertions(+), 71 deletions(-) create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/TestFonts.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/TestFonts.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontData.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontData.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFamilyAssets.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFamilyAssets.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFileStore.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFileStore.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontLoadException.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontLoadException.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSourceKind.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSourceKind.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSrcResolver.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSrcResolver.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontVariant.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontVariant.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsCache.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsCache.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceCatalog.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceCatalog.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceFamilyRecord.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceFamilyRecord.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/GetFontIntention.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/GetFontIntention.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/LoadFontSystem.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/LoadFontSystem.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/RuntimeFontAssetFactory.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/RuntimeFontAssetFactory.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/SceneFontRequest.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/SceneFontRequest.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontFileStoreShould.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontFileStoreShould.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontSrcResolverShould.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontSrcResolverShould.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/Fonts.Tests.asmref create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/Fonts.Tests.asmref.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontsourceCatalogShould.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontsourceCatalogShould.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/Fonts.PlayMode.Tests.asmref create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/Fonts.PlayMode.Tests.asmref.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/LoadFontSystemShould.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/LoadFontSystemShould.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/RuntimeFontAssetFactoryShould.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/RuntimeFontAssetFactoryShould.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/SceneFontRequestShould.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/SceneFontRequestShould.cs.meta create mode 100644 Explorer/Assets/DCL/PluginSystem/World/FontsLoadingPlugin.cs create mode 100644 Explorer/Assets/DCL/PluginSystem/World/FontsLoadingPlugin.cs.meta create mode 100644 Explorer/Assets/DCL/SDKComponents/SceneUI/Tests/UIDropdownReleaseSystemShould.cs create mode 100644 Explorer/Assets/DCL/SDKComponents/SceneUI/Tests/UIDropdownReleaseSystemShould.cs.meta create mode 100644 Explorer/Assets/DCL/SDKComponents/SceneUI/Tests/UIInputReleaseSystemShould.cs create mode 100644 Explorer/Assets/DCL/SDKComponents/SceneUI/Tests/UIInputReleaseSystemShould.cs.meta create mode 100644 Explorer/Assets/DCL/SDKComponents/SceneUI/Tests/UITextReleaseSystemShould.cs create mode 100644 Explorer/Assets/DCL/SDKComponents/SceneUI/Tests/UITextReleaseSystemShould.cs.meta create mode 100644 Explorer/Assets/DCL/SDKComponents/TextShape/System/ReleaseTextShapeSystem.cs create mode 100644 Explorer/Assets/DCL/SDKComponents/TextShape/System/ReleaseTextShapeSystem.cs.meta create mode 100644 Explorer/Assets/DCL/SDKComponents/TextShape/Tests.meta create mode 100644 Explorer/Assets/DCL/SDKComponents/TextShape/Tests/ReleaseTextShapeSystemShould.cs create mode 100644 Explorer/Assets/DCL/SDKComponents/TextShape/Tests/ReleaseTextShapeSystemShould.cs.meta create mode 100644 Explorer/Assets/DCL/SDKComponents/TextShape/Tests/TextShape.Tests.asmref create mode 100644 Explorer/Assets/DCL/SDKComponents/TextShape/Tests/TextShape.Tests.asmref.meta diff --git a/Explorer/Assets/AddressableAssetsData/AssetGroups/UI.asset b/Explorer/Assets/AddressableAssetsData/AssetGroups/UI.asset index dbd6cb7288d..0df6d83d447 100644 --- a/Explorer/Assets/AddressableAssetsData/AssetGroups/UI.asset +++ b/Explorer/Assets/AddressableAssetsData/AssetGroups/UI.asset @@ -580,6 +580,11 @@ MonoBehaviour: m_ReadOnly: 0 m_SerializedLabels: [] FlaggedDuringContentUpdateRestriction: 0 + - m_GUID: 96ae0a2159a39234f858ea23bdcc74ad + m_Address: Assets/TextMesh Pro/Fonts & Materials/Inter-SemiBold SDF.asset + m_ReadOnly: 0 + m_SerializedLabels: [] + FlaggedDuringContentUpdateRestriction: 0 - m_GUID: 989cf1173881645c598573447b2b56d8 m_Address: HidesRow m_ReadOnly: 0 diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/ECS.TestSuite.asmdef b/Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/ECS.TestSuite.asmdef index b9e17530ddf..cc650b040d3 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/ECS.TestSuite.asmdef +++ b/Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/ECS.TestSuite.asmdef @@ -13,7 +13,8 @@ "GUID:3c7b57a14671040bd8c549056adc04f5", "GUID:fa7b3fdbb04d67549916da7bd2af58ab", "GUID:e25ef972de004615a22937e739de2def", - "GUID:0f4c0f120707fb74497f5d581b9858f8" + "GUID:0f4c0f120707fb74497f5d581b9858f8", + "GUID:6055be8ebefd69e48b49212b09b47b2f" ], "includePlatforms": [ "Editor" diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/TestFonts.cs b/Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/TestFonts.cs new file mode 100644 index 00000000000..a1c0777aa19 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/TestFonts.cs @@ -0,0 +1,23 @@ +using System.IO; +using TMPro; +using UnityEngine; +using UnityEngine.TextCore.LowLevel; +using UnityEngine.TextCore.Text; + +namespace ECS.TestSuite +{ + public static class TestFonts + { + public static readonly string PATH = Path.Combine(Application.dataPath, "DCL/SDKComponents/Fonts/LiberationSans-Regular.ttf"); + + private const int SAMPLING_POINT_SIZE = 90; + private const int ATLAS_PADDING = 9; + private const int ATLAS_SIZE = 1024; + + public static TMP_FontAsset CreateTextMeshProFont() => + TMP_FontAsset.CreateFontAsset(PATH, 0, SAMPLING_POINT_SIZE, ATLAS_PADDING, GlyphRenderMode.SDFAA, ATLAS_SIZE, ATLAS_SIZE); + + public static FontAsset CreateUIToolkitFont() => + FontAsset.CreateFontAsset(PATH, 0, SAMPLING_POINT_SIZE, ATLAS_PADDING, GlyphRenderMode.SDFAA, ATLAS_SIZE, ATLAS_SIZE); + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/TestFonts.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/TestFonts.cs.meta new file mode 100644 index 00000000000..c30ab27d0a5 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/TestFonts.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c8169cb1bcfd439fa5bee03aa0b0f936 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Common/Components/CommonLoadingArguments.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Common/Components/CommonLoadingArguments.cs index e83c7fa65c1..8048a1c0e27 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Common/Components/CommonLoadingArguments.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Common/Components/CommonLoadingArguments.cs @@ -56,6 +56,13 @@ public CommonLoadingArguments(string url, URLSubdirectory customEmbeddedSubDirec CancellationTokenSource cancellationTokenSource = null) : this(URLAddress.FromString(url), customEmbeddedSubDirectory, timeout, attempts, permittedSources, currentSource, cancellationTokenSource) { } + public CommonLoadingArguments WithURL(URLAddress url) + { + CommonLoadingArguments copy = this; + copy.URL = url; + return copy; + } + // Always override attempts count for streamable assets as repetitions are handled in LoadSystemBase public static implicit operator CommonArguments(in CommonLoadingArguments commonLoadingArguments) => new (commonLoadingArguments.URL, RetryPolicy.WithRetries(1), timeout: commonLoadingArguments.Timeout); diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/DeferredLoading/AssetsDeferredLoadingSystem.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/DeferredLoading/AssetsDeferredLoadingSystem.cs index 1b201ddfa3e..fa2d96c83a7 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/DeferredLoading/AssetsDeferredLoadingSystem.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/DeferredLoading/AssetsDeferredLoadingSystem.cs @@ -4,6 +4,7 @@ using ECS.Groups; using ECS.StreamableLoading.AssetBundles; using ECS.StreamableLoading.AudioClips; +using ECS.StreamableLoading.Fonts; using ECS.StreamableLoading.GLTF; using ECS.StreamableLoading.NFTShapes; using ECS.StreamableLoading.Textures; @@ -29,6 +30,7 @@ static AssetsDeferredLoadingSystem() CreateQuery(), CreateQuery(), CreateQuery(), + CreateQuery(), }; } diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/DeferredLoading/Tests/DeferredLoadingSystemShould.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/DeferredLoading/Tests/DeferredLoadingSystemShould.cs index 8f512e5b554..f14a0fe7f26 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/DeferredLoading/Tests/DeferredLoadingSystemShould.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/DeferredLoading/Tests/DeferredLoadingSystemShould.cs @@ -2,6 +2,7 @@ using DCL.Optimization.PerformanceBudgeting; using ECS.Prioritization.Components; using ECS.StreamableLoading.Common.Components; +using ECS.StreamableLoading.Fonts; using ECS.StreamableLoading.Textures; using ECS.TestSuite; using NSubstitute; @@ -123,5 +124,27 @@ public void IntentionsAllowedWhenBudgetIsReleased() Assert.AreEqual(i < 8, world.Get(entities[i]).Value == StreamableLoadingState.Status.Allowed); } + + [Test] + public void AllowFontIntentions() + { + Entity entity = world.Create( + new GetFontIntention + { + Kind = FontSourceKind.FontsourceFamily, + Src = "Roboto", + CommonArguments = new CommonLoadingArguments(""), + }, + (IPartitionComponent)new PartitionComponent + { + Bucket = 0, + IsBehind = false, + IsDirty = true, + }, new StreamableLoadingState()); + + system.Update(0); + + Assert.That(world.Get(entity).Value, Is.EqualTo(StreamableLoadingState.Status.Allowed)); + } } } diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts.meta new file mode 100644 index 00000000000..a603aaa03ae --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ac05f0bd5a15477c7a4244e3d3a421d0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontData.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontData.cs new file mode 100644 index 00000000000..f5b6e70aea6 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontData.cs @@ -0,0 +1,20 @@ +using DCL.Diagnostics; +using DCL.Profiling; +using Unity.Profiling; + +namespace ECS.StreamableLoading.Fonts +{ + public class FontData : StreamableRefCountData + { + public FontData(FontFamilyAssets assets) : base(assets, ReportCategory.FONTS) { } + + protected override ref ProfilerCounterValue totalCount => ref ProfilingCounters.FontsAmount; + + protected override ref ProfilerCounterValue referencedCount => ref ProfilingCounters.FontsReferenced; + + protected override void DestroyObject() + { + Asset.Destroy(); + } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontData.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontData.cs.meta new file mode 100644 index 00000000000..7aecab6c66a --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontData.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 552ca3cae0756a9c1873cd6d54cc0094 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFamilyAssets.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFamilyAssets.cs new file mode 100644 index 00000000000..2cdcc0251fc --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFamilyAssets.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using TMPro; +using UnityEngine.TextCore.Text; +using Utility; + +namespace ECS.StreamableLoading.Fonts +{ + public class FontFamilyAssets + { + private readonly List textMeshProAssets; + private readonly List uiToolkitAssets; + + public TMP_FontAsset TextMeshProFont { get; } + + public FontAsset UIToolkitFont { get; } + + public FontFamilyAssets(TMP_FontAsset textMeshProFont, FontAsset uiToolkitFont, List textMeshProAssets, List uiToolkitAssets) + { + TextMeshProFont = textMeshProFont; + UIToolkitFont = uiToolkitFont; + this.textMeshProAssets = textMeshProAssets; + this.uiToolkitAssets = uiToolkitAssets; + } + + public void Destroy() => + Destroy(textMeshProAssets, uiToolkitAssets); + + public static void Destroy(List textMeshProAssets, List uiToolkitAssets) + { + foreach (TMP_FontAsset asset in textMeshProAssets) + { + TMP_ResourceManager.RemoveFontAsset(asset); + UnityObjectUtils.SafeDestroy(asset); + } + + foreach (FontAsset asset in uiToolkitAssets) + UnityObjectUtils.SafeDestroy(asset); + + textMeshProAssets.Clear(); + uiToolkitAssets.Clear(); + } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFamilyAssets.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFamilyAssets.cs.meta new file mode 100644 index 00000000000..5ae4641fd5d --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFamilyAssets.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a5782533918b7b8c1baf4effc6369c0b \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFileStore.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFileStore.cs new file mode 100644 index 00000000000..b0880ea4702 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFileStore.cs @@ -0,0 +1,107 @@ +using Cysharp.Threading.Tasks; +using DCL.Diagnostics; +using DCL.Optimization.Hashing; +using ECS.StreamableLoading.Cache.Disk; +using System; +using System.IO; +using System.Threading; +using UnityEngine; + +namespace ECS.StreamableLoading.Fonts +{ + // FreeType re-reads the file whenever the atlas needs glyphs: files stay on disk while their assets live + public class FontFileStore + { + public const int MAX_FILE_BYTES = 16 * 1024 * 1024; + + public const string TRUE_TYPE_EXTENSION = ".ttf"; + public const string OPEN_TYPE_EXTENSION = ".otf"; + + private const string DIRECTORY_NAME = "SceneFonts"; + + private const int SFNT_HEADER_BYTES = 12; + + private const uint SFNT_VERSION_TRUE_TYPE = 0x00010000; + private const uint SFNT_VERSION_CFF = 0x4F54544F; + private const uint SFNT_VERSION_APPLE_TRUE_TYPE = 0x74727565; + private const uint SFNT_VERSION_COLLECTION = 0x74746366; + + private readonly string directory; + + public FontFileStore(string directory) + { + this.directory = directory; + } + + public static FontFileStore InPersistentData() + { + var store = new FontFileStore(Path.Combine(Application.persistentDataPath, DIRECTORY_NAME)); + store.Clear(); + return store; + } + + public static bool LooksLikeFontFile(byte[] bytes) + { + if (bytes.Length < SFNT_HEADER_BYTES) + return false; + + uint version = ((uint)bytes[0] << 24) | ((uint)bytes[1] << 16) | ((uint)bytes[2] << 8) | bytes[3]; + + return version is SFNT_VERSION_TRUE_TYPE + or SFNT_VERSION_CFF + or SFNT_VERSION_APPLE_TRUE_TYPE + or SFNT_VERSION_COLLECTION; + } + + public void Clear() + { + if (!Directory.Exists(directory)) + return; + + try { Directory.Delete(directory, true); } + catch (Exception e) when (e is IOException or UnauthorizedAccessException) + { + ReportHub.LogWarning(ReportCategory.FONTS, $"The scene fonts under {directory} could not be removed: {e.Message}"); + } + } + + public async UniTask StoreAsync(byte[] bytes, CancellationToken ct) + { + await UniTask.SwitchToThreadPool(); + ct.ThrowIfCancellationRequested(); + + string path = Path.Combine(directory, FileName(bytes)); + + if (File.Exists(path)) + return path; + + Directory.CreateDirectory(directory); + + string temp = $"{path}.{Guid.NewGuid():N}.tmp"; + + try + { + File.WriteAllBytes(temp, bytes); + + try { File.Move(temp, path); } + catch (IOException) when (File.Exists(path)) + { + // Another download of the same bytes won the rename + } + } + finally + { + if (File.Exists(temp)) + File.Delete(temp); + } + + return path; + } + + private static string FileName(byte[] bytes) + { + using HashKey key = HashKey.FromOwnedMemory(SHA256Hashing.ComputeHash(bytes)); + return HashNamings.HashNameFrom(key, TRUE_TYPE_EXTENSION); + } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFileStore.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFileStore.cs.meta new file mode 100644 index 00000000000..904ee8c8a85 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontFileStore.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 87a460711ff440262b409eab0c2c0c14 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontLoadException.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontLoadException.cs new file mode 100644 index 00000000000..a374375d183 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontLoadException.cs @@ -0,0 +1,10 @@ +using ECS.StreamableLoading.Common; +using UnityEngine; + +namespace ECS.StreamableLoading.Fonts +{ + public class FontLoadException : StreamableLoadingException + { + public FontLoadException(string message) : base(LogType.Warning, message) { } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontLoadException.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontLoadException.cs.meta new file mode 100644 index 00000000000..46f9fef9e9f --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontLoadException.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a0510b46aa5ab842d9bf7b6d985f6219 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSourceKind.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSourceKind.cs new file mode 100644 index 00000000000..bdbb165b031 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSourceKind.cs @@ -0,0 +1,9 @@ +namespace ECS.StreamableLoading.Fonts +{ + public enum FontSourceKind : byte + { + File, + + FontsourceFamily, + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSourceKind.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSourceKind.cs.meta new file mode 100644 index 00000000000..9f877279662 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSourceKind.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9ae81c0d07d54a8487c3907f19a836ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSrcResolver.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSrcResolver.cs new file mode 100644 index 00000000000..354d5a924b1 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSrcResolver.cs @@ -0,0 +1,59 @@ +using CommunicationData.URLHelpers; +using DCL.Diagnostics; +using ECS.StreamableLoading.Common.Components; +using SceneRunner.Scene; +using System; + +namespace ECS.StreamableLoading.Fonts +{ + public static class FontSrcResolver + { + private const int ATTEMPTS_COUNT = 2; + + public static bool TryCreateIntention(string fontSrc, ISceneData sceneData, out GetFontIntention intention) + { + intention = default(GetFontIntention); + + if (LooksLikeFileReference(fontSrc)) + { + if (!sceneData.TryGetMediaUrl(fontSrc, out URLAddress url)) + { + ReportHub.LogWarning(ReportCategory.FONTS, $"font_src \"{fontSrc}\" is neither a content file of the scene {sceneData.SceneShortInfo} nor an allowed media URL, the built-in font stays"); + return false; + } + + intention = new GetFontIntention + { + Kind = FontSourceKind.File, + Src = fontSrc, + CommonArguments = new CommonLoadingArguments(url, attempts: ATTEMPTS_COUNT), + }; + + return true; + } + + string? familyId = FontsourceCatalog.ToFamilyId(fontSrc); + + if (familyId == null) + { + ReportHub.LogWarning(ReportCategory.FONTS, $"font_src \"{fontSrc}\" is neither a scene file, a URL nor a font family name, the built-in font stays"); + return false; + } + + intention = new GetFontIntention + { + Kind = FontSourceKind.FontsourceFamily, + Src = fontSrc, + CommonArguments = new CommonLoadingArguments(URLAddress.FromString(FontsourceCatalog.ApiUrl(familyId)), attempts: ATTEMPTS_COUNT), + }; + + return true; + } + + private static bool LooksLikeFileReference(string value) => + value.IndexOf('/') >= 0 + || value.IndexOf('\\') >= 0 + || value.EndsWith(FontFileStore.TRUE_TYPE_EXTENSION, StringComparison.OrdinalIgnoreCase) + || value.EndsWith(FontFileStore.OPEN_TYPE_EXTENSION, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSrcResolver.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSrcResolver.cs.meta new file mode 100644 index 00000000000..1c6d89878bb --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontSrcResolver.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e023af99984a3e870a19b26bfc9dd6f9 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontVariant.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontVariant.cs new file mode 100644 index 00000000000..89715522d63 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontVariant.cs @@ -0,0 +1,10 @@ +namespace ECS.StreamableLoading.Fonts +{ + public enum FontVariant : byte + { + Regular, + Bold, + Italic, + BoldItalic, + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontVariant.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontVariant.cs.meta new file mode 100644 index 00000000000..30ccbfec4fd --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontVariant.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3cd8e855303b4853baf317c2e4708396 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsCache.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsCache.cs new file mode 100644 index 00000000000..9de5285f4b5 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsCache.cs @@ -0,0 +1,11 @@ +using DCL.Profiling; +using ECS.StreamableLoading.Cache; +using Unity.Profiling; + +namespace ECS.StreamableLoading.Fonts +{ + public class FontsCache : RefCountStreamableCacheBase + { + protected override ref ProfilerCounterValue inCacheCount => ref ProfilingCounters.FontsInCache; + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsCache.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsCache.cs.meta new file mode 100644 index 00000000000..7d5136a1690 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsCache.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 17c108360dafe90439de0392aec99a78 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceCatalog.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceCatalog.cs new file mode 100644 index 00000000000..e7480ebc9f2 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceCatalog.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ECS.StreamableLoading.Fonts +{ + public static class FontsourceCatalog + { + public const string API_BASE_URL = "https://api.fontsource.org/v1/fonts/"; + + private const string PREFERRED_SUBSET = "latin"; + private const string STYLE_NORMAL = "normal"; + private const string STYLE_ITALIC = "italic"; + + private const string LATEST_VERSION_SEGMENT = "@latest/"; + + private static readonly string[] REGULAR_WEIGHTS = { "400", "500", "300", "600" }; + private static readonly string[] BOLD_WEIGHTS = { "700", "600", "800", "500", "900" }; + + public static string? ToFamilyId(string familyName) + { + var id = new StringBuilder(familyName.Length); + var pendingSeparator = false; + + foreach (char c in familyName) + { + if (char.IsWhiteSpace(c)) + { + pendingSeparator = id.Length > 0; + continue; + } + + if (!IsAsciiLetterOrDigit(c) && c != '-') + return null; + + if (pendingSeparator) + { + id.Append('-'); + pendingSeparator = false; + } + + id.Append(char.ToLowerInvariant(c)); + } + + return id.Length > 0 ? id.ToString() : null; + } + + public static string ApiUrl(string familyId) => + API_BASE_URL + familyId; + + public static bool TryGetVariantUrl(FontsourceFamilyRecord record, FontVariant variant, out string url) + { + (string[] weights, string style) = variant switch + { + FontVariant.Regular => (REGULAR_WEIGHTS, STYLE_NORMAL), + FontVariant.Bold => (BOLD_WEIGHTS, STYLE_NORMAL), + FontVariant.Italic => (REGULAR_WEIGHTS, STYLE_ITALIC), + FontVariant.BoldItalic => (BOLD_WEIGHTS, STYLE_ITALIC), + _ => throw new ArgumentOutOfRangeException(nameof(variant), variant, null), + }; + + foreach (string weight in weights) + { + if (TryGetTtf(record, weight, style, out url)) + return true; + } + + url = string.Empty; + return false; + } + + private static bool TryGetTtf(FontsourceFamilyRecord record, string weight, string style, out string url) + { + url = string.Empty; + + if (record.variants == null + || !record.variants.TryGetValue(weight, out Dictionary> styles) + || !styles.TryGetValue(style, out Dictionary subsets)) + return false; + + FontsourceFamilyRecord.Files? files = null; + + if (!subsets.TryGetValue(PREFERRED_SUBSET, out files) && record.subsets != null) + foreach (string subset in record.subsets) + if (subsets.TryGetValue(subset, out files)) + break; + + string? ttf = files?.url?.ttf; + + if (string.IsNullOrEmpty(ttf)) + return false; + + url = string.IsNullOrEmpty(record.npmVersion) + ? ttf + : ttf.Replace(LATEST_VERSION_SEGMENT, $"@{record.npmVersion}/"); + + return true; + } + + private static bool IsAsciiLetterOrDigit(char c) => + c is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '0' and <= '9'; + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceCatalog.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceCatalog.cs.meta new file mode 100644 index 00000000000..5c36cff2ad0 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceCatalog.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: afb79a4e5c55653598476adfb99e3263 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceFamilyRecord.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceFamilyRecord.cs new file mode 100644 index 00000000000..4898d5cf00a --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceFamilyRecord.cs @@ -0,0 +1,30 @@ +// ReSharper disable InconsistentNaming + +using System; +using System.Collections.Generic; + +namespace ECS.StreamableLoading.Fonts +{ + // https://api.fontsource.org/v1/fonts/{id} + [Serializable] + public class FontsourceFamilyRecord + { + public string? npmVersion; + + public List? subsets; + + public Dictionary>>? variants; + + [Serializable] + public class Files + { + public Urls? url; + } + + [Serializable] + public class Urls + { + public string? ttf; + } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceFamilyRecord.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceFamilyRecord.cs.meta new file mode 100644 index 00000000000..fb68951852c --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/FontsourceFamilyRecord.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: dd469624fc36466e89c1a4221c619bef +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/GetFontIntention.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/GetFontIntention.cs new file mode 100644 index 00000000000..607c133b60d --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/GetFontIntention.cs @@ -0,0 +1,37 @@ +using ECS.StreamableLoading.Common.Components; +using System; +using System.Threading; + +namespace ECS.StreamableLoading.Fonts +{ + public struct GetFontIntention : ILoadingIntention, IEquatable + { + public FontSourceKind Kind; + + public string Src; + + private int? hashCode; + + public CommonLoadingArguments CommonArguments { get; set; } + + public CancellationTokenSource CancellationTokenSource => CommonArguments.CancellationTokenSource; + + public bool Equals(GetFontIntention other) => + Kind == other.Kind && this.AreUrlEquals(other); + + public override bool Equals(object? obj) => + obj is GetFontIntention other && Equals(other); + + public override int GetHashCode() + { + if (hashCode != null) + return hashCode.Value; + + hashCode = HashCode.Combine(Kind, CommonArguments.URL); + return hashCode.Value; + } + + public override string ToString() => + $"Get Font Intention: {Src} ({Kind}) {CommonArguments.URL}"; + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/GetFontIntention.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/GetFontIntention.cs.meta new file mode 100644 index 00000000000..0c503fd5aba --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/GetFontIntention.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e62ef47d2e3dde2be82f1a854fbf3f9a \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/LoadFontSystem.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/LoadFontSystem.cs new file mode 100644 index 00000000000..e8343706060 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/LoadFontSystem.cs @@ -0,0 +1,98 @@ +using Arch.Core; +using Arch.SystemGroups; +using CommunicationData.URLHelpers; +using Cysharp.Threading.Tasks; +using DCL.Diagnostics; +using DCL.WebRequests; +using ECS.Prioritization.Components; +using ECS.StreamableLoading.Cache; +using ECS.StreamableLoading.Common.Components; +using ECS.StreamableLoading.Common.Systems; +using System; +using System.Threading; + +namespace ECS.StreamableLoading.Fonts +{ + [UpdateInGroup(typeof(StreamableLoadingGroup))] + [LogCategory(ReportCategory.FONTS)] + public partial class LoadFontSystem : LoadSystemBase + { + private readonly IWebRequestController webRequestController; + private readonly RuntimeFontAssetFactory fontAssetFactory; + private readonly FontFileStore fileStore; + + internal LoadFontSystem(World world, IStreamableCache cache, IWebRequestController webRequestController, + RuntimeFontAssetFactory fontAssetFactory, FontFileStore fileStore) : base(world, cache) + { + this.webRequestController = webRequestController; + this.fontAssetFactory = fontAssetFactory; + this.fileStore = fileStore; + } + + protected override async UniTask> FlowInternalAsync(GetFontIntention intention, StreamableLoadingState state, IPartitionComponent partition, CancellationToken ct) + { + string regularPath; + string? boldPath = null; + string? italicPath = null; + string? boldItalicPath = null; + + if (intention.Kind == FontSourceKind.FontsourceFamily) + (regularPath, boldPath, italicPath, boldItalicPath) = await DownloadFamilyAsync(intention, ct); + else + regularPath = await DownloadAsync(intention.CommonArguments, intention.Src, ct); + + await UniTask.SwitchToMainThread(ct); + + FontFamilyAssets? assets = fontAssetFactory.Create(intention.Src, regularPath, boldPath, italicPath, boldItalicPath); + + if (assets == null) + throw new FontLoadException($"\"{intention.Src}\" is not a font FreeType can read"); + + return new StreamableLoadingResult(new FontData(assets)); + } + + private async UniTask<(string regularPath, string? boldPath, string? italicPath, string? boldItalicPath)> DownloadFamilyAsync(GetFontIntention intention, CancellationToken ct) + { + FontsourceFamilyRecord record = await webRequestController.GetAsync(intention.CommonArguments, ct, GetReportData()) + .CreateFromNewtonsoftJsonAsync(); + + if (!FontsourceCatalog.TryGetVariantUrl(record, FontVariant.Regular, out string regularUrl)) + throw new FontLoadException($"The font family \"{intention.Src}\" has no regular face on Fontsource"); + + string regularPath = await DownloadAsync(intention.CommonArguments.WithURL(URLAddress.FromString(regularUrl)), intention.Src, ct); + + (string? boldPath, string? italicPath, string? boldItalicPath) = await UniTask.WhenAll( + DownloadVariantIfPresentAsync(record, FontVariant.Bold, intention.CommonArguments, intention.Src, ct), + DownloadVariantIfPresentAsync(record, FontVariant.Italic, intention.CommonArguments, intention.Src, ct), + DownloadVariantIfPresentAsync(record, FontVariant.BoldItalic, intention.CommonArguments, intention.Src, ct)); + + return (regularPath, boldPath, italicPath, boldItalicPath); + } + + private async UniTask DownloadAsync(CommonArguments arguments, string src, CancellationToken ct) + { + byte[] bytes = await webRequestController.GetAsync(arguments, ct, GetReportData()).GetDataCopyAsync(); + + if (bytes.Length > FontFileStore.MAX_FILE_BYTES) + throw new FontLoadException($"\"{src}\": {arguments.URL} is {bytes.Length} bytes, scene fonts are capped at {FontFileStore.MAX_FILE_BYTES} bytes"); + + if (!FontFileStore.LooksLikeFontFile(bytes)) + throw new FontLoadException($"\"{src}\": {arguments.URL} is not a TrueType/OpenType font file"); + + return await fileStore.StoreAsync(bytes, ct); + } + + private async UniTask DownloadVariantIfPresentAsync(FontsourceFamilyRecord record, FontVariant variant, CommonLoadingArguments commonArguments, string src, CancellationToken ct) + { + if (!FontsourceCatalog.TryGetVariantUrl(record, variant, out string url)) + return null; + + try { return await DownloadAsync(commonArguments.WithURL(URLAddress.FromString(url)), src, ct); } + catch (Exception e) when (e is not OperationCanceledException) + { + ReportHub.LogWarning(GetReportData(), $"\"{src}\": the {variant} face failed to load, the regular face stands in: {e.Message}"); + return null; + } + } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/LoadFontSystem.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/LoadFontSystem.cs.meta new file mode 100644 index 00000000000..984c0ac3cec --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/LoadFontSystem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 44adfe487c3ce551fabaaa6c9922690f \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/RuntimeFontAssetFactory.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/RuntimeFontAssetFactory.cs new file mode 100644 index 00000000000..00fbac22722 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/RuntimeFontAssetFactory.cs @@ -0,0 +1,131 @@ +using DCL.Diagnostics; +using System.Collections.Generic; +using TMPro; +using UnityEngine; +using UnityEngine.TextCore.LowLevel; +using UnityEngine.TextCore.Text; +using Utility; + +namespace ECS.StreamableLoading.Fonts +{ + public class RuntimeFontAssetFactory + { + private const int SAMPLING_POINT_SIZE = 90; + + private const int ATLAS_PADDING = 9; + + private const int ATLAS_SIZE = 1024; + private const GlyphRenderMode RENDER_MODE = GlyphRenderMode.SDFAA; + + private const int SDF_PACKING_MODIFIER = 1; + + private const int REGULAR_WEIGHT_INDEX = 4; + private const int BOLD_WEIGHT_INDEX = 7; + + private readonly TMP_FontAsset referenceFont; + + public RuntimeFontAssetFactory(TMP_FontAsset referenceFont) + { + this.referenceFont = referenceFont; + } + + public FontFamilyAssets? Create(string assetName, string regularFilePath, string? boldFilePath, string? italicFilePath, string? boldItalicFilePath) + { + var textMeshProAssets = new List(4); + var uiToolkitAssets = new List(4); + + TMP_FontAsset? textMeshProRegular = CreateTextMeshProAsset(assetName, regularFilePath, textMeshProAssets); + FontAsset? uiToolkitRegular = CreateUIToolkitAsset(assetName, regularFilePath, uiToolkitAssets); + + if (textMeshProRegular == null || uiToolkitRegular == null) + { + FontFamilyAssets.Destroy(textMeshProAssets, uiToolkitAssets); + return null; + } + + WireVariant(assetName, boldFilePath, FontVariant.Bold, textMeshProRegular, uiToolkitRegular, textMeshProAssets, uiToolkitAssets); + WireVariant(assetName, italicFilePath, FontVariant.Italic, textMeshProRegular, uiToolkitRegular, textMeshProAssets, uiToolkitAssets); + WireVariant(assetName, boldItalicFilePath, FontVariant.BoldItalic, textMeshProRegular, uiToolkitRegular, textMeshProAssets, uiToolkitAssets); + + return new FontFamilyAssets(textMeshProRegular, uiToolkitRegular, textMeshProAssets, uiToolkitAssets); + } + + private void WireVariant(string assetName, string? filePath, FontVariant variant, TMP_FontAsset textMeshProRegular, FontAsset uiToolkitRegular, + List textMeshProAssets, List uiToolkitAssets) + { + if (filePath == null) + return; + + string variantName = $"{assetName} {variant}"; + TMP_FontAsset? textMeshProVariant = CreateTextMeshProAsset(variantName, filePath, textMeshProAssets); + FontAsset? uiToolkitVariant = CreateUIToolkitAsset(variantName, filePath, uiToolkitAssets); + + if (textMeshProVariant == null || uiToolkitVariant == null) + { + ReportHub.LogWarning(ReportCategory.FONTS, $"The {variant} face of the scene font \"{assetName}\" could not be read, the regular face stands in"); + return; + } + + TMP_FontWeightPair[] textMeshProTable = textMeshProRegular.fontWeightTable; + FontWeightPair[] uiToolkitTable = uiToolkitRegular.fontWeightTable; + + switch (variant) + { + case FontVariant.Bold: + textMeshProTable[BOLD_WEIGHT_INDEX].regularTypeface = textMeshProVariant; + uiToolkitTable[BOLD_WEIGHT_INDEX].regularTypeface = uiToolkitVariant; + break; + case FontVariant.Italic: + textMeshProTable[REGULAR_WEIGHT_INDEX].italicTypeface = textMeshProVariant; + uiToolkitTable[REGULAR_WEIGHT_INDEX].italicTypeface = uiToolkitVariant; + break; + case FontVariant.BoldItalic: + textMeshProTable[BOLD_WEIGHT_INDEX].italicTypeface = textMeshProVariant; + uiToolkitTable[BOLD_WEIGHT_INDEX].italicTypeface = uiToolkitVariant; + break; + } + } + + private TMP_FontAsset? CreateTextMeshProAsset(string name, string filePath, List owned) + { + TMP_FontAsset? asset = TMP_FontAsset.CreateFontAsset(filePath, 0, SAMPLING_POINT_SIZE, ATLAS_PADDING, RENDER_MODE, ATLAS_SIZE, ATLAS_SIZE); + + if (asset == null) + return null; + + asset.name = name; + owned.Add(asset); + + // CreateFontAsset looks up "TextMeshPro/Mobile/Distance Field" by name: that shader is in the always-included list + Material generated = asset.material; + ShaderUtilities.GetShaderPropertyIDs(); + + var material = new Material(referenceFont.material) { name = $"{name} Material" }; + material.SetTexture(ShaderUtilities.ID_MainTex, asset.atlasTexture); + material.SetFloat(ShaderUtilities.ID_TextureWidth, ATLAS_SIZE); + material.SetFloat(ShaderUtilities.ID_TextureHeight, ATLAS_SIZE); + material.SetFloat(ShaderUtilities.ID_GradientScale, ATLAS_PADDING + SDF_PACKING_MODIFIER); + material.SetFloat(ShaderUtilities.ID_WeightNormal, asset.normalStyle); + material.SetFloat(ShaderUtilities.ID_WeightBold, asset.boldStyle); + + asset.material = material; + UnityObjectUtils.SafeDestroy(generated); + + asset.fallbackFontAssetTable = new List { referenceFont }; + + return asset; + } + + private static FontAsset? CreateUIToolkitAsset(string name, string filePath, List owned) + { + FontAsset? asset = FontAsset.CreateFontAsset(filePath, 0, SAMPLING_POINT_SIZE, ATLAS_PADDING, RENDER_MODE, ATLAS_SIZE, ATLAS_SIZE); + + if (asset == null) + return null; + + asset.name = name; + owned.Add(asset); + return asset; + } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/RuntimeFontAssetFactory.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/RuntimeFontAssetFactory.cs.meta new file mode 100644 index 00000000000..38e49f43447 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/RuntimeFontAssetFactory.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 53bf15a034142f9e188769a702f155b3 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/SceneFontRequest.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/SceneFontRequest.cs new file mode 100644 index 00000000000..a56b8e48d43 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/SceneFontRequest.cs @@ -0,0 +1,66 @@ +using Arch.Core; +using ECS.Prioritization.Components; +using ECS.StreamableLoading.Cache; +using ECS.StreamableLoading.Common.Components; +using SceneRunner.Scene; +using System; +using FontPromise = ECS.StreamableLoading.Common.AssetPromise; + +namespace ECS.StreamableLoading.Fonts +{ + public struct SceneFontRequest + { + public string? Src { get; private set; } + + public FontPromise? Promise { get; private set; } + + public bool Update(World world, ISceneData sceneData, string? fontSrc, IPartitionComponent partition) + { + fontSrc = string.IsNullOrWhiteSpace(fontSrc) ? null : fontSrc.Trim(); + + if (string.Equals(fontSrc, Src, StringComparison.Ordinal)) + return false; + + Release(world); + Src = fontSrc; + + if (fontSrc != null && FontSrcResolver.TryCreateIntention(fontSrc, sceneData, out GetFontIntention intention)) + Promise = FontPromise.Create(world, intention, partition); + + return true; + } + + public bool TryConsume(World world, out FontFamilyAssets? assets) + { + assets = null; + + if (Promise == null || Promise.Value.IsConsumed) + return false; + + FontPromise promise = Promise.Value; + + if (!promise.TryConsume(world, out StreamableLoadingResult result)) + return false; + + Promise = promise; + + if (result.Succeeded) + assets = result.Asset!.Asset; + + return true; + } + + public void Release(World world) + { + Src = null; + + if (Promise == null) + return; + + FontPromise promise = Promise.Value; + promise.TryDereference(world); + promise.ForgetLoading(world); + Promise = null; + } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/SceneFontRequest.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/SceneFontRequest.cs.meta new file mode 100644 index 00000000000..f651d5879f8 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/SceneFontRequest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2eabad57607e50c088891056c53ad23c \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests.meta new file mode 100644 index 00000000000..433956c75f7 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c23c85ebdf57bd5c7bb444c0089fb7f9 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontFileStoreShould.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontFileStoreShould.cs new file mode 100644 index 00000000000..07137369bc7 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontFileStoreShould.cs @@ -0,0 +1,121 @@ +using NUnit.Framework; +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using UnityEngine; + +namespace ECS.StreamableLoading.Fonts.Tests +{ + [TestFixture] + public class FontFileStoreShould + { + private static readonly byte[] BYTES = { 1, 2, 3 }; + private static readonly byte[] OTHER_BYTES = { 4, 5, 6 }; + + private string directory = null!; + private FontFileStore store = null!; + + [SetUp] + public void SetUp() + { + directory = Path.Combine(Application.temporaryCachePath, nameof(FontFileStoreShould), Guid.NewGuid().ToString("N")); + store = new FontFileStore(directory); + } + + [TearDown] + public void TearDown() + { + if (Directory.Exists(directory)) + Directory.Delete(directory, true); + } + + [TestCase(new byte[] { 0x00, 0x01, 0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 0 }, true)] + [TestCase(new byte[] { 0x4F, 0x54, 0x54, 0x4F, 0, 0, 0, 0, 0, 0, 0, 0 }, true)] + [TestCase(new byte[] { 0x74, 0x72, 0x75, 0x65, 0, 0, 0, 0, 0, 0, 0, 0 }, true)] + [TestCase(new byte[] { 0x74, 0x74, 0x63, 0x66, 0, 0, 0, 0, 0, 0, 0, 0 }, true)] + [TestCase(new byte[] { 0x00, 0x01, 0x00 }, false)] + [TestCase(new byte[] { 0x77, 0x4F, 0x46, 0x46, 0, 0, 0, 0, 0, 0, 0, 0 }, false)] + public void RecognizeSfntHeaders(byte[] bytes, bool expected) + { + bool looksLikeFont = FontFileStore.LooksLikeFontFile(bytes); + + Assert.That(looksLikeFont, Is.EqualTo(expected)); + } + + [Test] + public void RejectAnHtmlErrorPage() + { + bool looksLikeFont = FontFileStore.LooksLikeFontFile(Encoding.ASCII.GetBytes("")); + + Assert.That(looksLikeFont, Is.False); + } + + [Test] + public async Task StoreTheBytesUnderTheirHash() + { + string path = await store.StoreAsync(BYTES, CancellationToken.None); + + Assert.That(Path.GetDirectoryName(path), Is.EqualTo(directory)); + Assert.That(Path.GetExtension(path), Is.EqualTo(".ttf")); + Assert.That(File.ReadAllBytes(path), Is.EqualTo(BYTES)); + Assert.That(Directory.GetFiles(directory), Has.Length.EqualTo(1)); + } + + [Test] + public async Task ReuseTheFileOfTheSameBytes() + { + string first = await store.StoreAsync(BYTES, CancellationToken.None); + + string second = await store.StoreAsync(BYTES, CancellationToken.None); + + Assert.That(second, Is.EqualTo(first)); + Assert.That(Directory.GetFiles(directory), Has.Length.EqualTo(1)); + } + + [Test] + public async Task KeepDifferentBytesInDifferentFiles() + { + string first = await store.StoreAsync(BYTES, CancellationToken.None); + + string second = await store.StoreAsync(OTHER_BYTES, CancellationToken.None); + + Assert.That(second, Is.Not.EqualTo(first)); + Assert.That(File.ReadAllBytes(second), Is.EqualTo(OTHER_BYTES)); + Assert.That(Directory.GetFiles(directory), Has.Length.EqualTo(2)); + } + + [Test] + public void WriteNothingWhenCancelled() + { + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + Assert.CatchAsync(async () => await store.StoreAsync(BYTES, cts.Token)); + Assert.That(Directory.Exists(directory), Is.False); + } + + [Test] + public async Task RemoveEveryFileOnClear() + { + await store.StoreAsync(BYTES, CancellationToken.None); + + store.Clear(); + + Assert.That(Directory.Exists(directory), Is.False); + } + + [Test] + public async Task StoreAgainAfterClear() + { + string first = await store.StoreAsync(BYTES, CancellationToken.None); + store.Clear(); + + string second = await store.StoreAsync(BYTES, CancellationToken.None); + + Assert.That(second, Is.EqualTo(first)); + Assert.That(File.ReadAllBytes(second), Is.EqualTo(BYTES)); + } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontFileStoreShould.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontFileStoreShould.cs.meta new file mode 100644 index 00000000000..aa3de35c59d --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontFileStoreShould.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0e80247e86df829f98c0b17d3185ce79 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontSrcResolverShould.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontSrcResolverShould.cs new file mode 100644 index 00000000000..9169c191258 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontSrcResolverShould.cs @@ -0,0 +1,101 @@ +using CommunicationData.URLHelpers; +using ECS.StreamableLoading.Common.Components; +using NSubstitute; +using NUnit.Framework; +using SceneRunner.Scene; + +namespace ECS.StreamableLoading.Fonts.Tests +{ + [TestFixture] + public class FontSrcResolverShould + { + private const string CONTENT_FILE = "fonts/Lobster-Regular.ttf"; + private const string CONTENT_URL = "https://peer.decentraland.org/content/contents/bafyfont"; + private const string MEDIA_URL = "https://fonts.gstatic.com/s/pacifico/v23/FwZY7-Qmy14u9lezJ96A.ttf"; + + private ISceneData sceneData = null!; + + [SetUp] + public void SetUp() + { + sceneData = Substitute.For(); + + sceneData.TryGetMediaUrl(CONTENT_FILE, out Arg.Any()) + .Returns(x => + { + x[1] = URLAddress.FromString(CONTENT_URL); + return true; + }); + + sceneData.TryGetMediaUrl(MEDIA_URL, out Arg.Any()) + .Returns(x => + { + x[1] = URLAddress.FromString(MEDIA_URL); + return true; + }); + } + + [Test] + public void ResolveAContentFile() + { + bool resolved = FontSrcResolver.TryCreateIntention(CONTENT_FILE, sceneData, out GetFontIntention intention); + + Assert.That(resolved, Is.True); + Assert.That(intention.Kind, Is.EqualTo(FontSourceKind.File)); + Assert.That(intention.Src, Is.EqualTo(CONTENT_FILE)); + Assert.That(intention.CommonArguments.URL.Value, Is.EqualTo(CONTENT_URL)); + } + + [Test] + public void ResolveAnAllowedMediaUrl() + { + bool resolved = FontSrcResolver.TryCreateIntention(MEDIA_URL, sceneData, out GetFontIntention intention); + + Assert.That(resolved, Is.True); + Assert.That(intention.Kind, Is.EqualTo(FontSourceKind.File)); + Assert.That(intention.CommonArguments.URL.Value, Is.EqualTo(MEDIA_URL)); + } + + [Test] + public void ResolveAFamilyNameToFontsource() + { + bool resolved = FontSrcResolver.TryCreateIntention("Playfair Display", sceneData, out GetFontIntention intention); + + Assert.That(resolved, Is.True); + Assert.That(intention.Kind, Is.EqualTo(FontSourceKind.FontsourceFamily)); + Assert.That(intention.Src, Is.EqualTo("Playfair Display")); + Assert.That(intention.CommonArguments.URL.Value, Is.EqualTo(FontsourceCatalog.API_BASE_URL + "playfair-display")); + sceneData.DidNotReceiveWithAnyArgs().TryGetMediaUrl(default!, out Arg.Any()); + } + + [TestCase("no such/file.ttf")] + [TestCase("https://example.com/not-allowed.ttf")] + [TestCase("$$$")] + public void KeepTheBuiltInFontWhenNothingResolves(string fontSrc) + { + bool resolved = FontSrcResolver.TryCreateIntention(fontSrc, sceneData, out GetFontIntention _); + + Assert.That(resolved, Is.False); + } + + [Test] + public void ShareTheRequestBetweenTextsAskingForTheSameFile() + { + FontSrcResolver.TryCreateIntention(CONTENT_FILE, sceneData, out GetFontIntention first); + FontSrcResolver.TryCreateIntention(CONTENT_FILE, sceneData, out GetFontIntention second); + + Assert.That(first.Equals(second), Is.True); + Assert.That(first.GetHashCode(), Is.EqualTo(second.GetHashCode())); + } + + [Test] + public void KeepSourcesOfDifferentKindsApart() + { + var file = new GetFontIntention { Kind = FontSourceKind.File, Src = MEDIA_URL, CommonArguments = new CommonLoadingArguments(URLAddress.FromString(MEDIA_URL)) }; + var family = new GetFontIntention { Kind = FontSourceKind.FontsourceFamily, Src = MEDIA_URL, CommonArguments = new CommonLoadingArguments(URLAddress.FromString(MEDIA_URL)) }; + + Assert.That(file.Equals(family), Is.False); + Assert.That(file.GetHashCode(), Is.Not.EqualTo(family.GetHashCode())); + } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontSrcResolverShould.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontSrcResolverShould.cs.meta new file mode 100644 index 00000000000..dfe299b27e9 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontSrcResolverShould.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 986fba806388bcfd9b554c0ebae37ada \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/Fonts.Tests.asmref b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/Fonts.Tests.asmref new file mode 100644 index 00000000000..5aa9fc5db6f --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/Fonts.Tests.asmref @@ -0,0 +1,3 @@ +{ + "reference": "GUID:da80994a355e49d5b84f91c0a84a721f" +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/Fonts.Tests.asmref.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/Fonts.Tests.asmref.meta new file mode 100644 index 00000000000..7391e8d8067 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/Fonts.Tests.asmref.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6074e1060d5336ed1951311d023eac42 +AssemblyDefinitionReferenceImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontsourceCatalogShould.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontsourceCatalogShould.cs new file mode 100644 index 00000000000..96107ea06d8 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontsourceCatalogShould.cs @@ -0,0 +1,140 @@ +using NUnit.Framework; +using System.Collections.Generic; + +namespace ECS.StreamableLoading.Fonts.Tests +{ + [TestFixture] + public class FontsourceCatalogShould + { + private const string VERSION = "5.3.0"; + + [TestCase("Roboto", "roboto")] + [TestCase("Playfair Display", "playfair-display")] + [TestCase(" Noto Sans JP ", "noto-sans-jp")] + [TestCase("Open-Sans", "open-sans")] + public void MapFamilyNameToFontsourceId(string familyName, string expectedId) + { + string? id = FontsourceCatalog.ToFamilyId(familyName); + + Assert.That(id, Is.EqualTo(expectedId)); + } + + [TestCase("")] + [TestCase(" ")] + [TestCase("Font$Name")] + [TestCase("Über Font")] + [TestCase("fonts.ttf")] + public void RejectNamesThatAreNotFamilyNames(string familyName) + { + string? id = FontsourceCatalog.ToFamilyId(familyName); + + Assert.That(id, Is.Null); + } + + [Test] + public void PointTheRecordUrlAtTheFamilyId() + { + string url = FontsourceCatalog.ApiUrl("playfair-display"); + + Assert.That(url, Is.EqualTo(FontsourceCatalog.API_BASE_URL + "playfair-display")); + } + + [Test] + public void PickTheRegularWeightAndPinTheVersion() + { + FontsourceFamilyRecord record = Record(("400", "normal", "latin"), ("700", "normal", "latin")); + + string? regular = VariantUrl(record, FontVariant.Regular); + string? bold = VariantUrl(record, FontVariant.Bold); + + Assert.That(regular, Is.EqualTo(Url("400", "normal", "latin", VERSION))); + Assert.That(bold, Is.EqualTo(Url("700", "normal", "latin", VERSION))); + } + + [Test] + public void FallBackToTheClosestWeights() + { + FontsourceFamilyRecord record = Record(("300", "normal", "latin"), ("600", "normal", "latin"), ("300", "italic", "latin")); + + string? regular = VariantUrl(record, FontVariant.Regular); + string? bold = VariantUrl(record, FontVariant.Bold); + string? italic = VariantUrl(record, FontVariant.Italic); + string? boldItalic = VariantUrl(record, FontVariant.BoldItalic); + + Assert.That(regular, Is.EqualTo(Url("300", "normal", "latin", VERSION))); + Assert.That(bold, Is.EqualTo(Url("600", "normal", "latin", VERSION))); + Assert.That(italic, Is.EqualTo(Url("300", "italic", "latin", VERSION))); + Assert.That(boldItalic, Is.Null); + } + + [Test] + public void PreferLatinButAcceptTheFirstListedSubset() + { + FontsourceFamilyRecord record = Record(("400", "normal", "cyrillic"), ("400", "normal", "greek")); + record.subsets = new List { "greek", "cyrillic" }; + + string? regular = VariantUrl(record, FontVariant.Regular); + + Assert.That(regular, Is.EqualTo(Url("400", "normal", "greek", VERSION))); + } + + [Test] + public void KeepTheUrlWhenTheRecordHasNoVersion() + { + FontsourceFamilyRecord record = Record(("400", "normal", "latin")); + record.npmVersion = null; + + string? regular = VariantUrl(record, FontVariant.Regular); + + Assert.That(regular, Is.EqualTo(Url("400", "normal", "latin", "latest"))); + } + + [Test] + public void FindNoUrlWhenTheVariantHasNoTtf() + { + FontsourceFamilyRecord record = Record(("400", "normal", "latin")); + record.variants!["400"]["normal"]["latin"].url!.ttf = null; + + string? regular = VariantUrl(record, FontVariant.Regular); + + Assert.That(regular, Is.Null); + } + + [Test] + public void FindNoUrlInAnEmptyRecord() + { + string? regular = VariantUrl(new FontsourceFamilyRecord(), FontVariant.Regular); + + Assert.That(regular, Is.Null); + } + + private static string? VariantUrl(FontsourceFamilyRecord record, FontVariant variant) => + FontsourceCatalog.TryGetVariantUrl(record, variant, out string url) ? url : null; + + private static string Url(string weight, string style, string subset, string version) => + $"https://cdn.jsdelivr.net/fontsource/fonts/test@{version}/{subset}-{weight}-{style}.ttf"; + + private static FontsourceFamilyRecord Record(params (string weight, string style, string subset)[] faces) + { + var record = new FontsourceFamilyRecord + { + npmVersion = VERSION, + subsets = new List { "latin" }, + variants = new Dictionary>>(), + }; + + foreach ((string weight, string style, string subset) in faces) + { + if (!record.variants.TryGetValue(weight, out Dictionary> styles)) + record.variants[weight] = styles = new Dictionary>(); + + if (!styles.TryGetValue(style, out Dictionary subsets)) + styles[style] = subsets = new Dictionary(); + + subsets[subset] = new FontsourceFamilyRecord.Files { url = new FontsourceFamilyRecord.Urls { ttf = Url(weight, style, subset, "latest") } }; + } + + return record; + } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontsourceCatalogShould.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontsourceCatalogShould.cs.meta new file mode 100644 index 00000000000..ea691055083 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/FontsourceCatalogShould.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8c6be76b268d4717086f08833d1c02b4 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode.meta new file mode 100644 index 00000000000..0ba3b445a0e --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 82a8feafb7726e973a8d94bb74bff8f2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/Fonts.PlayMode.Tests.asmref b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/Fonts.PlayMode.Tests.asmref new file mode 100644 index 00000000000..ff46abcec01 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/Fonts.PlayMode.Tests.asmref @@ -0,0 +1,3 @@ +{ + "reference": "GUID:7213a1d35667f9b40be6aca8cbfcee23" +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/Fonts.PlayMode.Tests.asmref.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/Fonts.PlayMode.Tests.asmref.meta new file mode 100644 index 00000000000..6a57d649238 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/Fonts.PlayMode.Tests.asmref.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 45a87b9a6edb221e1afe9541eeb7c544 +AssemblyDefinitionReferenceImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/LoadFontSystemShould.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/LoadFontSystemShould.cs new file mode 100644 index 00000000000..54bae7a37f5 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/LoadFontSystemShould.cs @@ -0,0 +1,70 @@ +using ECS.StreamableLoading.Common.Components; +using ECS.StreamableLoading.Tests; +using ECS.TestSuite; +using NUnit.Framework; +using System.IO; +using TMPro; +using UnityEngine; + +namespace ECS.StreamableLoading.Fonts.Tests +{ + [TestFixture] + public class LoadFontSystemShould : LoadSystemBaseShould + { + private TMP_FontAsset? referenceFont; + + private string successPath => $"file://{TestFonts.PATH}"; + private string failPath => $"file://{Application.dataPath + "/DCL/SDKComponents/Fonts/non_existing.ttf"}"; + private string wrongTypePath => $"file://{Application.dataPath + "/../TestResources/CRDT/arraybuffer.test"}"; + + [TearDown] + public void DestroyReferenceFont() + { + if (referenceFont != null) + Object.DestroyImmediate(referenceFont); + } + + protected override GetFontIntention CreateSuccessIntention() => + new () + { + Kind = FontSourceKind.File, + Src = "LiberationSans-Regular.ttf", + CommonArguments = new CommonLoadingArguments(successPath), + }; + + protected override GetFontIntention CreateNotFoundIntention() => + new () + { + Kind = FontSourceKind.File, + Src = "non_existing.ttf", + CommonArguments = new CommonLoadingArguments(failPath), + }; + + protected override GetFontIntention CreateWrongTypeIntention() => + new () + { + Kind = FontSourceKind.File, + Src = "arraybuffer.test", + CommonArguments = new CommonLoadingArguments(wrongTypePath), + }; + + protected override LoadFontSystem CreateSystem() + { + TMP_FontAsset font = TestFonts.CreateTextMeshProFont(); + referenceFont = font; + + return new LoadFontSystem(world, cache, TestWebRequestController.INSTANCE, new RuntimeFontAssetFactory(font), + new FontFileStore(Path.Combine(Application.temporaryCachePath, "SceneFontsTests"))); + } + + protected override void AssertSuccess(FontData data) + { + FontFamilyAssets assets = data.Asset; + + Assert.That(assets.TextMeshProFont, Is.Not.Null); + Assert.That(assets.TextMeshProFont.faceInfo.familyName, Is.EqualTo("Liberation Sans")); + Assert.That(assets.UIToolkitFont, Is.Not.Null); + Assert.That(assets.UIToolkitFont.faceInfo.familyName, Is.EqualTo("Liberation Sans")); + } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/LoadFontSystemShould.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/LoadFontSystemShould.cs.meta new file mode 100644 index 00000000000..3b123ba3c87 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/PlayMode/LoadFontSystemShould.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b1cbca15546f595a1b3286e79d4ac49d \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/RuntimeFontAssetFactoryShould.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/RuntimeFontAssetFactoryShould.cs new file mode 100644 index 00000000000..144c3c674b2 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/RuntimeFontAssetFactoryShould.cs @@ -0,0 +1,114 @@ +using ECS.TestSuite; +using NUnit.Framework; +using System.IO; +using TMPro; +using UnityEngine; +using UnityEngine.TextCore.Text; + +namespace ECS.StreamableLoading.Fonts.Tests +{ + [TestFixture] + public class RuntimeFontAssetFactoryShould + { + private const string ASSET_NAME = "Liberation"; + private const string FAMILY_NAME = "Liberation Sans"; + private const int REGULAR_WEIGHT_INDEX = 4; + private const int BOLD_WEIGHT_INDEX = 7; + + private static readonly string NOT_A_FONT_PATH = Path.Combine(Application.dataPath, "../TestResources/CRDT/arraybuffer.test"); + + private TMP_FontAsset referenceFont = null!; + private RuntimeFontAssetFactory factory = null!; + private FontFamilyAssets? assets; + + [SetUp] + public void SetUp() + { + referenceFont = TestFonts.CreateTextMeshProFont(); + factory = new RuntimeFontAssetFactory(referenceFont); + } + + [TearDown] + public void TearDown() + { + assets?.Destroy(); + Object.DestroyImmediate(referenceFont); + } + + [Test] + public void BuildBothAssetsFromTheRegularFace() + { + assets = factory.Create(ASSET_NAME, TestFonts.PATH, null, null, null); + + Assert.That(assets, Is.Not.Null); + Assert.That(assets!.TextMeshProFont.name, Is.EqualTo(ASSET_NAME)); + Assert.That(assets.TextMeshProFont.faceInfo.familyName, Is.EqualTo(FAMILY_NAME)); + Assert.That(assets.UIToolkitFont.faceInfo.familyName, Is.EqualTo(FAMILY_NAME)); + Assert.That(assets.TextMeshProFont.fontWeightTable[BOLD_WEIGHT_INDEX].regularTypeface, Is.Null); + } + + [Test] + public void WireTheVariantsIntoTheWeightTables() + { + assets = factory.Create(ASSET_NAME, TestFonts.PATH, TestFonts.PATH, TestFonts.PATH, TestFonts.PATH); + + TMP_FontWeightPair[] textMeshProTable = assets!.TextMeshProFont.fontWeightTable; + Assert.That(textMeshProTable[BOLD_WEIGHT_INDEX].regularTypeface.name, Is.EqualTo($"{ASSET_NAME} {FontVariant.Bold}")); + Assert.That(textMeshProTable[REGULAR_WEIGHT_INDEX].italicTypeface.name, Is.EqualTo($"{ASSET_NAME} {FontVariant.Italic}")); + Assert.That(textMeshProTable[BOLD_WEIGHT_INDEX].italicTypeface.name, Is.EqualTo($"{ASSET_NAME} {FontVariant.BoldItalic}")); + + FontWeightPair[] uiToolkitTable = assets.UIToolkitFont.fontWeightTable; + Assert.That(uiToolkitTable[BOLD_WEIGHT_INDEX].regularTypeface.name, Is.EqualTo($"{ASSET_NAME} {FontVariant.Bold}")); + Assert.That(uiToolkitTable[REGULAR_WEIGHT_INDEX].italicTypeface.name, Is.EqualTo($"{ASSET_NAME} {FontVariant.Italic}")); + Assert.That(uiToolkitTable[BOLD_WEIGHT_INDEX].italicTypeface.name, Is.EqualTo($"{ASSET_NAME} {FontVariant.BoldItalic}")); + } + + [Test] + public void BuildNothingWhenTheRegularFaceIsNotAFont() + { + assets = factory.Create(ASSET_NAME, NOT_A_FONT_PATH, null, null, null); + + Assert.That(assets, Is.Null); + } + + [Test] + public void KeepTheRegularFaceWhenAVariantIsNotAFont() + { + assets = factory.Create(ASSET_NAME, TestFonts.PATH, NOT_A_FONT_PATH, null, null); + + Assert.That(assets, Is.Not.Null); + Assert.That(assets!.TextMeshProFont.fontWeightTable[BOLD_WEIGHT_INDEX].regularTypeface, Is.Null); + } + + [Test] + public void TakeTheMaterialAndFallbackFromTheReferenceFont() + { + assets = factory.Create(ASSET_NAME, TestFonts.PATH, null, null, null); + + TMP_FontAsset font = assets!.TextMeshProFont; + Assert.That(font.material, Is.Not.SameAs(referenceFont.material)); + Assert.That(font.material.name, Is.EqualTo($"{ASSET_NAME} Material")); + Assert.That(font.material.shader, Is.EqualTo(referenceFont.material.shader)); + Assert.That(font.material.mainTexture, Is.EqualTo(font.atlasTexture)); + Assert.That(font.fallbackFontAssetTable, Is.EqualTo(new[] { referenceFont })); + } + + [Test] + public void DestroyEveryAssetItOwns() + { + assets = factory.Create(ASSET_NAME, TestFonts.PATH, TestFonts.PATH, null, null); + TMP_FontAsset regular = assets!.TextMeshProFont; + TMP_FontAsset bold = regular.fontWeightTable[BOLD_WEIGHT_INDEX].regularTypeface; + Material material = regular.material; + FontAsset uiToolkitRegular = assets.UIToolkitFont; + + assets.Destroy(); + assets = null; + + Assert.That(regular == null, Is.True); + Assert.That(bold == null, Is.True); + Assert.That(material == null, Is.True); + Assert.That(uiToolkitRegular == null, Is.True); + } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/RuntimeFontAssetFactoryShould.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/RuntimeFontAssetFactoryShould.cs.meta new file mode 100644 index 00000000000..270916e41a1 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/RuntimeFontAssetFactoryShould.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b381800ee38f425080f783a0f6f72e50 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/SceneFontRequestShould.cs b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/SceneFontRequestShould.cs new file mode 100644 index 00000000000..429ad138549 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/SceneFontRequestShould.cs @@ -0,0 +1,212 @@ +using Arch.Core; +using CommunicationData.URLHelpers; +using DCL.Diagnostics; +using ECS.Prioritization.Components; +using ECS.StreamableLoading.Common.Components; +using ECS.TestSuite; +using NSubstitute; +using NUnit.Framework; +using SceneRunner.Scene; +using System.Collections.Generic; +using System.Threading; +using TMPro; +using UnityEngine.TextCore.Text; + +namespace ECS.StreamableLoading.Fonts.Tests +{ + [TestFixture] + public class SceneFontRequestShould + { + private const string FAMILY_SRC = "Roboto"; + private const string FILE_SRC = "fonts/Lobster-Regular.ttf"; + + private World world = null!; + private ISceneData sceneData = null!; + private SceneFontRequest request; + private FontFamilyAssets? family; + + [SetUp] + public void SetUp() + { + world = World.Create(); + sceneData = Substitute.For(); + + sceneData.TryGetMediaUrl(FILE_SRC, out Arg.Any()) + .Returns(x => + { + x[1] = URLAddress.FromString("https://peer.decentraland.org/content/contents/bafyfont"); + return true; + }); + + request = new SceneFontRequest(); + } + + [TearDown] + public void TearDown() + { + world.Dispose(); + family?.Destroy(); + } + + [Test] + public void CreateAPromiseWhenTheSourceResolves() + { + bool updated = Update(FAMILY_SRC); + + Assert.That(updated, Is.True); + Assert.That(request.Src, Is.EqualTo(FAMILY_SRC)); + Assert.That(request.Promise, Is.Not.Null); + Entity promiseEntity = request.Promise!.Value.Entity; + Assert.That(world.IsAlive(promiseEntity), Is.True); + Assert.That(world.Get(promiseEntity).Kind, Is.EqualTo(FontSourceKind.FontsourceFamily)); + } + + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + public void HoldNothingForAnEmptySource(string? fontSrc) + { + bool updated = Update(fontSrc); + + Assert.That(updated, Is.False); + Assert.That(request.Src, Is.Null); + Assert.That(request.Promise, Is.Null); + } + + [Test] + public void TrimTheSource() + { + bool updated = Update($" {FILE_SRC} "); + + Assert.That(updated, Is.True); + Assert.That(request.Src, Is.EqualTo(FILE_SRC)); + Assert.That(request.Promise, Is.Not.Null); + } + + [Test] + public void KeepThePromiseForTheSamePaddedSource() + { + Update(FILE_SRC); + Entity promiseEntity = request.Promise!.Value.Entity; + + bool updated = Update($" {FILE_SRC} "); + + Assert.That(updated, Is.False); + Assert.That(request.Promise!.Value.Entity, Is.EqualTo(promiseEntity)); + } + + [Test] + public void HoldNoPromiseWhenTheSourceDoesNotResolve() + { + bool updated = Update("no such/file.ttf"); + + Assert.That(updated, Is.True); + Assert.That(request.Src, Is.EqualTo("no such/file.ttf")); + Assert.That(request.Promise, Is.Null); + } + + [Test] + public void KeepThePromiseForTheSameSource() + { + Update(FAMILY_SRC); + Entity promiseEntity = request.Promise!.Value.Entity; + + bool updated = Update(FAMILY_SRC); + + Assert.That(updated, Is.False); + Assert.That(request.Promise!.Value.Entity, Is.EqualTo(promiseEntity)); + } + + [Test] + public void ReplaceThePromiseWhenTheSourceChanges() + { + Update(FAMILY_SRC); + Entity firstPromiseEntity = request.Promise!.Value.Entity; + + bool updated = Update(FILE_SRC); + + Assert.That(updated, Is.True); + Assert.That(world.IsAlive(firstPromiseEntity), Is.False); + Assert.That(request.Src, Is.EqualTo(FILE_SRC)); + Assert.That(world.Get(request.Promise!.Value.Entity).Kind, Is.EqualTo(FontSourceKind.File)); + } + + [Test] + public void ConsumeNothingWhileTheLoadIsPending() + { + Update(FAMILY_SRC); + + bool consumed = request.TryConsume(world, out FontFamilyAssets? _); + + Assert.That(consumed, Is.False); + } + + [Test] + public void ConsumeTheLoadedFamilyOnce() + { + Update(FAMILY_SRC); + family = CreateFamily(); + var data = new FontData(family); + ((IStreamableRefCountData)data).AddReference(); + world.Add(request.Promise!.Value.Entity, new StreamableLoadingResult(data)); + + bool consumed = request.TryConsume(world, out FontFamilyAssets? assets); + bool consumedAgain = request.TryConsume(world, out FontFamilyAssets? _); + + Assert.That(consumed, Is.True); + Assert.That(assets, Is.SameAs(family)); + Assert.That(request.Promise!.Value.IsConsumed, Is.True); + Assert.That(consumedAgain, Is.False); + } + + [Test] + public void ConsumeAFailedLoadAsNoFamily() + { + Update(FAMILY_SRC); + world.Add(request.Promise!.Value.Entity, new StreamableLoadingResult(ReportData.UNSPECIFIED, new FontLoadException("test"))); + + bool consumed = request.TryConsume(world, out FontFamilyAssets? assets); + + Assert.That(consumed, Is.True); + Assert.That(assets, Is.Null); + } + + [Test] + public void CancelThePendingLoadOnRelease() + { + Update(FAMILY_SRC); + Entity promiseEntity = request.Promise!.Value.Entity; + CancellationToken ct = world.Get(promiseEntity).CommonArguments.CancellationToken; + + request.Release(world); + + Assert.That(request.Src, Is.Null); + Assert.That(request.Promise, Is.Null); + Assert.That(world.IsAlive(promiseEntity), Is.False); + Assert.That(ct.IsCancellationRequested, Is.True); + } + + [Test] + public void StartOverAfterRelease() + { + Update(FAMILY_SRC); + request.Release(world); + + bool updated = Update(FAMILY_SRC); + + Assert.That(updated, Is.True); + Assert.That(request.Promise, Is.Not.Null); + } + + private bool Update(string? fontSrc) => + request.Update(world, sceneData, fontSrc, PartitionComponent.TOP_PRIORITY); + + private static FontFamilyAssets CreateFamily() + { + TMP_FontAsset textMeshProFont = TestFonts.CreateTextMeshProFont(); + FontAsset uiToolkitFont = TestFonts.CreateUIToolkitFont(); + + return new FontFamilyAssets(textMeshProFont, uiToolkitFont, new List { textMeshProFont }, new List { uiToolkitFont }); + } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/SceneFontRequestShould.cs.meta b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/SceneFontRequestShould.cs.meta new file mode 100644 index 00000000000..9d0ad9700d0 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/ECS/Unity/StreamableLoading/Fonts/Tests/SceneFontRequestShould.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fbb9175161e24a7aa0f741f70154deb9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/Infrastructure/Global/StaticContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/StaticContainer.cs index 3f8195772e0..63752de6b7b 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/StaticContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/StaticContainer.cs @@ -289,6 +289,7 @@ public UniTask InitializeAsync(StaticSettings settings, CancellationToken ct) new TransformsPlugin(sharedDependencies, exposedPlayerTransform, exposedGlobalDataContainer.ExposedCameraData), new BillboardPlugin(exposedGlobalDataContainer.ExposedCameraData), new NFTShapePlugin(decentralandUrlsSource, container.assetsProvisioner, sharedDependencies.FrameTimeBudget, componentsContainer.ComponentPoolsRegistry, container.WebRequestsContainer.WebRequestController, container.MediaContainer.mediaFactoryBuilder), + new FontsLoadingPlugin(container.WebRequestsContainer.WebRequestController, container.CacheCleaner, container.assetsProvisioner), new TextShapePlugin(sharedDependencies.FrameTimeBudget, container.CacheCleaner, componentsContainer.ComponentPoolsRegistry, assetsProvisioner), new MaterialsPlugin(sharedDependencies, container.MediaContainer.mediaFactoryBuilder), textureResolvePlugin, diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Tests/PlayMode/Integration Tests Container.asset b/Explorer/Assets/DCL/Infrastructure/Global/Tests/PlayMode/Integration Tests Container.asset index 0e7d59987da..84bd1fed8a2 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Tests/PlayMode/Integration Tests Container.asset +++ b/Explorer/Assets/DCL/Infrastructure/Global/Tests/PlayMode/Integration Tests Container.asset @@ -32,6 +32,7 @@ MonoBehaviour: - rid: 4753057106328551425 - rid: 6784895265395441664 - rid: 857448817791336450 + - rid: 857448817791336451 - rid: 732305993255354463 references: version: 2 @@ -67,6 +68,15 @@ MonoBehaviour: m_SubObjectType: m_SubObjectGUID: m_EditorAssetChanged: 0 + - rid: 857448817791336451 + type: {class: FontsLoadingPlugin/Settings, ns: DCL.PluginSystem.World, asm: DCL.Plugins} + data: + k__BackingField: + m_AssetGUID: 96ae0a2159a39234f858ea23bdcc74ad + m_SubObjectName: + m_SubObjectType: + m_SubObjectGUID: + m_EditorAssetChanged: 0 - rid: 892605786557251584 type: {class: NFTShapePluginSettings, ns: DCL.PluginSystem.Global, asm: DCL.Plugins} data: diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportCategory.cs b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportCategory.cs index 7f81ec9a86e..aefc67f2ca3 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportCategory.cs +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportCategory.cs @@ -132,6 +132,11 @@ public static class ReportCategory /// public const string TEXTURES = nameof(TEXTURES); + /// + /// The fonts scenes load through `font_src` + /// + public const string FONTS = nameof(FONTS); + /// /// Everything related to GLTF /// diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsDevelopment.asset b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsDevelopment.asset index d754fd10be9..8ea288d80a9 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsDevelopment.asset +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsDevelopment.asset @@ -728,6 +728,14 @@ MonoBehaviour: Severity: 4 - Category: CREDITS_PURCHASE Severity: 1 + - Category: FONTS + Severity: 0 + - Category: FONTS + Severity: 4 + - Category: FONTS + Severity: 1 + - Category: FONTS + Severity: 2 sentryMatrix: entries: [] debounceEnabled: 1 diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsProduction.asset b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsProduction.asset index bc34015f241..5003d7cacbc 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsProduction.asset +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/ReportsHandlingSettingsProduction.asset @@ -468,6 +468,10 @@ MonoBehaviour: Severity: 1 - Category: CREDITS_PURCHASE Severity: 4 + - Category: FONTS + Severity: 4 + - Category: FONTS + Severity: 1 sentryMatrix: entries: - Category: AUDIO_SOURCES @@ -900,6 +904,10 @@ MonoBehaviour: Severity: 1 - Category: WEARABLE Severity: 1 + - Category: FONTS + Severity: 4 + - Category: FONTS + Severity: 1 debounceEnabled: 1 isSentryEnabled: 1 isDebugLogEnabled: 1 diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ProfilingCounters.cs b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ProfilingCounters.cs index cbbcb0c4790..8af858b6801 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ProfilingCounters.cs +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Profiling/ProfilingCounters.cs @@ -82,6 +82,15 @@ public static class ProfilingCounters public static ProfilerCounterValue AudioClipsReferenced = new (MEMORY, "AudioClips Referenced", ProfilerMarkerDataUnit.Count); + public static ProfilerCounterValue FontsAmount = + new (MEMORY, "Scene Fonts", ProfilerMarkerDataUnit.Count); + + public static ProfilerCounterValue FontsInCache = + new (MEMORY, "Scene Fonts In Cache", ProfilerMarkerDataUnit.Count); + + public static ProfilerCounterValue FontsReferenced = + new (MEMORY, "Scene Fonts Referenced", ProfilerMarkerDataUnit.Count); + public static ProfilerCounterValue ProfilesInCache = new (MEMORY, "Profiles In Cache", ProfilerMarkerDataUnit.Count); @@ -120,6 +129,10 @@ public static void CleanAllCounters() AudioClipsInCache.Value = 0; AudioClipsReferenced.Value = 0; + FontsAmount.Value = 0; + FontsInCache.Value = 0; + FontsReferenced.Value = 0; + ProfilesInCache.Value = 0; } } diff --git a/Explorer/Assets/DCL/PluginSystem/Global/Plugin Settings.asset b/Explorer/Assets/DCL/PluginSystem/Global/Plugin Settings.asset index ef4d0b514c0..95b0f0bf334 100644 --- a/Explorer/Assets/DCL/PluginSystem/Global/Plugin Settings.asset +++ b/Explorer/Assets/DCL/PluginSystem/Global/Plugin Settings.asset @@ -85,6 +85,7 @@ MonoBehaviour: - rid: 6784895264912310272 - rid: 7162477752737071106 - rid: 857448815798779906 + - rid: 857448815798779907 - rid: 7493825610294857301 - rid: 3511243270148325472 - rid: 1709771649487732831 @@ -280,6 +281,15 @@ MonoBehaviour: m_SubObjectType: m_SubObjectGUID: m_EditorAssetChanged: 0 + - rid: 857448815798779907 + type: {class: FontsLoadingPlugin/Settings, ns: DCL.PluginSystem.World, asm: DCL.Plugins} + data: + k__BackingField: + m_AssetGUID: 96ae0a2159a39234f858ea23bdcc74ad + m_SubObjectName: + m_SubObjectType: + m_SubObjectGUID: + m_EditorAssetChanged: 0 - rid: 892605770607624195 type: {class: NFTShapePluginSettings, ns: DCL.PluginSystem.Global, asm: DCL.Plugins} data: diff --git a/Explorer/Assets/DCL/PluginSystem/World/FontsLoadingPlugin.cs b/Explorer/Assets/DCL/PluginSystem/World/FontsLoadingPlugin.cs new file mode 100644 index 00000000000..527c5f7d52d --- /dev/null +++ b/Explorer/Assets/DCL/PluginSystem/World/FontsLoadingPlugin.cs @@ -0,0 +1,62 @@ +using Arch.SystemGroups; +using Cysharp.Threading.Tasks; +using DCL.AssetsProvision; +using DCL.PluginSystem.World.Dependencies; +using DCL.ResourcesUnloading; +using DCL.WebRequests; +using ECS.LifeCycle; +using ECS.StreamableLoading.Fonts; +using SceneRunner.Scene; +using System; +using System.Collections.Generic; +using System.Threading; +using TMPro; +using UnityEngine; +using UnityEngine.AddressableAssets; + +namespace DCL.PluginSystem.World +{ + public class FontsLoadingPlugin : IDCLWorldPlugin + { + private readonly IWebRequestController webRequestController; + private readonly IAssetsProvisioner assetsProvisioner; + private readonly FontsCache fontsCache; + private readonly FontFileStore fontFileStore; + + private ProvidedAsset referenceFont; + private RuntimeFontAssetFactory fontAssetFactory = null!; + + public FontsLoadingPlugin(IWebRequestController webRequestController, CacheCleaner cacheCleaner, IAssetsProvisioner assetsProvisioner) + { + this.webRequestController = webRequestController; + this.assetsProvisioner = assetsProvisioner; + + fontsCache = new FontsCache(); + cacheCleaner.Register(fontsCache); + fontFileStore = FontFileStore.InPersistentData(); + } + + public void Dispose() + { + referenceFont.Dispose(); + } + + public async UniTask InitializeAsync(Settings settings, CancellationToken ct) + { + referenceFont = await assetsProvisioner.ProvideMainAssetAsync(settings.ReferenceFont, ct); + fontAssetFactory = new RuntimeFontAssetFactory(referenceFont.Value); + } + + public void InjectToWorld(ref ArchSystemsWorldBuilder builder, in ECSWorldInstanceSharedDependencies sharedDependencies, in SystemsDependencies systemsDependencies, in PersistentEntities persistentEntities, List finalizeWorldSystems, List sceneIsCurrentListeners) + { + LoadFontSystem.InjectToWorld(ref builder, fontsCache, webRequestController, fontAssetFactory, fontFileStore); + } + + [Serializable] + public class Settings : IDCLPluginSettings + { + [field: SerializeField] + public AssetReferenceT ReferenceFont { get; private set; } = null!; + } + } +} diff --git a/Explorer/Assets/DCL/PluginSystem/World/FontsLoadingPlugin.cs.meta b/Explorer/Assets/DCL/PluginSystem/World/FontsLoadingPlugin.cs.meta new file mode 100644 index 00000000000..40c50f43a35 --- /dev/null +++ b/Explorer/Assets/DCL/PluginSystem/World/FontsLoadingPlugin.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fb172349421b47e59e7635f0969ab943 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/PluginSystem/World/SceneUIPlugin.cs b/Explorer/Assets/DCL/PluginSystem/World/SceneUIPlugin.cs index 72385709e3e..ef779a51096 100644 --- a/Explorer/Assets/DCL/PluginSystem/World/SceneUIPlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/World/SceneUIPlugin.cs @@ -89,14 +89,14 @@ public void InjectToWorld(ref ArchSystemsWorldBuilder builder, UITransformSortingSystem.InjectToWorld(ref builder, sharedDependencies.EntitiesMap); sceneIsCurrentListeners.Add(UITransformUpdateSystem.InjectToWorld(ref builder, uiDocument, sharedDependencies.SceneStateProvider, persistentEntities.SceneRoot)); UITransformReleaseSystem.InjectToWorld(ref builder, componentPoolsRegistry); - UITextInstantiationSystem.InjectToWorld(ref builder, componentPoolsRegistry, styleFontDefinitions); - UITextReleaseSystem.InjectToWorld(ref builder, componentPoolsRegistry); + UITextInstantiationSystem.InjectToWorld(ref builder, componentPoolsRegistry, styleFontDefinitions, sharedDependencies.SceneData, sharedDependencies.ScenePartition); + finalizeWorldSystems.Add(UITextReleaseSystem.InjectToWorld(ref builder, componentPoolsRegistry)); UIBackgroundInstantiationSystem.InjectToWorld(ref builder, componentPoolsRegistry, sharedDependencies.SceneData, frameTimeBudgetProvider, memoryBudgetProvider); finalizeWorldSystems.Add(UIBackgroundReleaseSystem.InjectToWorld(ref builder, componentPoolsRegistry)); - UIInputInstantiationSystem.InjectToWorld(ref builder, componentPoolsRegistry, sharedDependencies.EcsToCRDTWriter, inputBlock, styleFontDefinitions); - UIInputReleaseSystem.InjectToWorld(ref builder, componentPoolsRegistry); - UIDropdownInstantiationSystem.InjectToWorld(ref builder, componentPoolsRegistry, sharedDependencies.EcsToCRDTWriter, styleFontDefinitions); - UIDropdownReleaseSystem.InjectToWorld(ref builder, componentPoolsRegistry); + UIInputInstantiationSystem.InjectToWorld(ref builder, componentPoolsRegistry, sharedDependencies.EcsToCRDTWriter, inputBlock, styleFontDefinitions, sharedDependencies.SceneData, sharedDependencies.ScenePartition); + finalizeWorldSystems.Add(UIInputReleaseSystem.InjectToWorld(ref builder, componentPoolsRegistry)); + UIDropdownInstantiationSystem.InjectToWorld(ref builder, componentPoolsRegistry, sharedDependencies.EcsToCRDTWriter, styleFontDefinitions, sharedDependencies.SceneData, sharedDependencies.ScenePartition); + finalizeWorldSystems.Add(UIDropdownReleaseSystem.InjectToWorld(ref builder, componentPoolsRegistry)); UIPointerEventsSystem.InjectToWorld(ref builder, sharedDependencies.SceneStateProvider, sharedDependencies.EcsToCRDTWriter); UICanvasInformationSystem.InjectToWorld(ref builder, sharedDependencies.EcsToCRDTWriter, uiDocument); UIFixPbPointerEventsSystem.InjectToWorld(ref builder); diff --git a/Explorer/Assets/DCL/PluginSystem/World/TextShapePlugin.cs b/Explorer/Assets/DCL/PluginSystem/World/TextShapePlugin.cs index b1a12740c34..f8f38ef284f 100644 --- a/Explorer/Assets/DCL/PluginSystem/World/TextShapePlugin.cs +++ b/Explorer/Assets/DCL/PluginSystem/World/TextShapePlugin.cs @@ -72,10 +72,11 @@ public void InjectToWorld(ref ArchSystemsWorldBuilder builder, { var buffer = sharedDependencies.EntityEventsBuilder.Rent(); - InstantiateTextShapeSystem.InjectToWorld(ref builder, textMeshProPool, fontsStorage, materialPropertyBlock, instantiationFrameTimeBudgetProvider, buffer); - UpdateTextShapeSystem.InjectToWorld(ref builder, fontsStorage, materialPropertyBlock, buffer, sharedDependencies.SceneData); + InstantiateTextShapeSystem.InjectToWorld(ref builder, textMeshProPool, fontsStorage, materialPropertyBlock, instantiationFrameTimeBudgetProvider, buffer, sharedDependencies.SceneData, sharedDependencies.ScenePartition); + UpdateTextShapeSystem.InjectToWorld(ref builder, fontsStorage, materialPropertyBlock, buffer, sharedDependencies.SceneData, sharedDependencies.ScenePartition); VisibilityTextShapeSystem.InjectToWorld(ref builder, buffer); + finalizeWorldSystems.Add(ReleaseTextShapeSystem.InjectToWorld(ref builder, fontsStorage, textMeshProPool)); finalizeWorldSystems.RegisterReleasePoolableComponentSystem(ref builder, componentPoolsRegistry); } diff --git a/Explorer/Assets/DCL/ResourcesUnloading/CacheCleaner.cs b/Explorer/Assets/DCL/ResourcesUnloading/CacheCleaner.cs index c6c03fbbdc7..f38f14158eb 100644 --- a/Explorer/Assets/DCL/ResourcesUnloading/CacheCleaner.cs +++ b/Explorer/Assets/DCL/ResourcesUnloading/CacheCleaner.cs @@ -13,6 +13,7 @@ using ECS.StreamableLoading.AudioClips; using ECS.StreamableLoading.Cache; using ECS.StreamableLoading.Cache.InMemory; +using ECS.StreamableLoading.Fonts; using ECS.StreamableLoading.GLTF; using ECS.StreamableLoading.NFTShapes; using ECS.StreamableLoading.Textures; @@ -31,6 +32,7 @@ public class CacheCleaner : ICacheCleaner private const int TEXTURE_UNLOAD_CHUNK = 1; private const int NFT_SHAPE_UNLOAD_CHUNK = 1; private const int AUDIO_CLIP_UNLOAD_CHUNK = 100; + private const int FONT_UNLOAD_CHUNK = 1; private const int PROFILE_UNLOAD_CHUNK = 10; private readonly IPerformanceBudget fpsCapBudget; @@ -46,6 +48,7 @@ public class CacheCleaner : ICacheCleaner private IStreamableCache? texturesCache; private ILODCache? lodCache; private IStreamableCache? audioClipsCache; + private IStreamableCache? fontsCache; private IAttachmentsAssetsCache? wearableAssetsCache; private IWearableStorage? wearableStorage; private ITrimmedWearableStorage? trimmedWearableStorage; @@ -81,6 +84,7 @@ public void UnloadCache(bool budgeted = true) texturesCache?.Unload(budgetToUse, budgeted ? TEXTURE_UNLOAD_CHUNK : int.MaxValue); audioClipsCache!.Unload(budgetToUse, budgeted ? AUDIO_CLIP_UNLOAD_CHUNK : int.MaxValue); + fontsCache?.Unload(budgetToUse, budgeted ? FONT_UNLOAD_CHUNK : int.MaxValue); wearableAssetsCache!.Unload(budgetToUse, budgeted ? WEARABLES_UNLOAD_CHUNK : int.MaxValue); wearableStorage!.Unload(budgetToUse); trimmedWearableStorage!.Unload(budgetToUse); @@ -144,6 +148,9 @@ public void Register(ISizedStreamableCache tex public void Register(IStreamableCache audioClipsCache) => this.audioClipsCache = audioClipsCache; + public void Register(IStreamableCache fontsCache) => + this.fontsCache = fontsCache; + public void Register(IWearableStorage storage) => wearableStorage = storage; diff --git a/Explorer/Assets/DCL/SDKComponents/SceneUI/Components/UIDropdownComponent.cs b/Explorer/Assets/DCL/SDKComponents/SceneUI/Components/UIDropdownComponent.cs index 008e711515b..fd41c903f3f 100644 --- a/Explorer/Assets/DCL/SDKComponents/SceneUI/Components/UIDropdownComponent.cs +++ b/Explorer/Assets/DCL/SDKComponents/SceneUI/Components/UIDropdownComponent.cs @@ -1,6 +1,8 @@ -using System; using DCL.SDKComponents.SceneUI.Utils; +using ECS.StreamableLoading.Fonts; +using System; using UnityEngine.UIElements; +using FontAsset = UnityEngine.TextCore.Text.FontAsset; namespace DCL.SDKComponents.SceneUI.Components { @@ -11,6 +13,10 @@ public class UIDropdownComponent public bool IsOnValueChangedTriggered; public int LastIndexSetByScene; + public SceneFontRequest FontRequest; + + public FontAsset? CustomFont; + internal Action? cachedScheduledAction; public void Initialize(string dropdownName) @@ -22,6 +28,8 @@ public void Initialize(string dropdownName) IsOnValueChangedTriggered = false; LastIndexSetByScene = int.MinValue; // -1 is used for the case of 'accept Empty value' + FontRequest = default(SceneFontRequest); + CustomFont = null; this.RegisterDropdownCallbacks(); } diff --git a/Explorer/Assets/DCL/SDKComponents/SceneUI/Components/UIInputComponent.cs b/Explorer/Assets/DCL/SDKComponents/SceneUI/Components/UIInputComponent.cs index 62a1b1cc280..3f635c12be5 100644 --- a/Explorer/Assets/DCL/SDKComponents/SceneUI/Components/UIInputComponent.cs +++ b/Explorer/Assets/DCL/SDKComponents/SceneUI/Components/UIInputComponent.cs @@ -1,8 +1,10 @@ using DCL.Input; using DCL.SDKComponents.SceneUI.Classes; using DCL.SDKComponents.SceneUI.Utils; +using ECS.StreamableLoading.Fonts; using UnityEngine; using UnityEngine.UIElements; +using FontAsset = UnityEngine.TextCore.Text.FontAsset; namespace DCL.SDKComponents.SceneUI.Components { @@ -14,6 +16,10 @@ public class UIInputComponent public bool IsOnValueChangedTriggered; public bool IsOnSubmitTriggered; + public SceneFontRequest FontRequest; + + public FontAsset? CustomFont; + internal EventCallback> currentOnValueChanged = static _ => { }; internal EventCallback currentOnSubmit = static _ => { }; internal EventCallback currentOnFocusIn = static _ => { }; @@ -37,6 +43,8 @@ public void Initialize( IsOnValueChangedTriggered = false; IsOnSubmitTriggered = false; + FontRequest = default(SceneFontRequest); + CustomFont = null; this.RegisterInputCallbacks(inputBlock); } diff --git a/Explorer/Assets/DCL/SDKComponents/SceneUI/Components/UITextComponent.cs b/Explorer/Assets/DCL/SDKComponents/SceneUI/Components/UITextComponent.cs index c993755ec55..1e65694001b 100644 --- a/Explorer/Assets/DCL/SDKComponents/SceneUI/Components/UITextComponent.cs +++ b/Explorer/Assets/DCL/SDKComponents/SceneUI/Components/UITextComponent.cs @@ -1,5 +1,7 @@ using DCL.Optimization.Pools; +using ECS.StreamableLoading.Fonts; using System; +using UnityEngine.TextCore.Text; using UnityEngine.UIElements; namespace DCL.SDKComponents.SceneUI.Components @@ -8,6 +10,10 @@ public struct UITextComponent: IPoolableComponentProvider