diff --git a/Core/GDCore/Extensions/Builtin/SceneExtension.cpp b/Core/GDCore/Extensions/Builtin/SceneExtension.cpp index 411b4d7d1..58247fe9d 100644 --- a/Core/GDCore/Extensions/Builtin/SceneExtension.cpp +++ b/Core/GDCore/Extensions/Builtin/SceneExtension.cpp @@ -216,6 +216,48 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsSceneExtension( .AddCodeOnlyParameter("currentScene", "") .AddParameter("sceneName", _("Scene name")) .MarkAsAdvanced(); + + extension + .AddAction( + "LoadObjectAssets", + _("Preload object"), + _("Preload an object resources in background."), + _("Preload object _PARAM1_ in background"), + "", + "res/actions/hourglass_black.svg", + "res/actions/hourglass_black.svg") + .SetHelpPath("/all-features/resources-loading") + .AddCodeOnlyParameter("currentScene", "") + .AddParameter("string", _("Object name")) + .MarkAsAdvanced(); + + extension + .AddAction( + "UnloadObjectAssets", + _("Unload object"), + _("Unload an object resources. The \"resource preloading\" property must be set to \"preload with an action\" for this action to actually unload resources."), + _("Unload object _PARAM1_"), + "", + "res/actions/hourglass_black.svg", + "res/actions/hourglass_black.svg") + .SetHelpPath("/all-features/resources-loading") + .AddCodeOnlyParameter("currentScene", "") + .AddParameter("string", _("Object name")) + .MarkAsAdvanced(); + + extension + .AddCondition( + "AreObjectAssetsLoaded", + _("Object preloaded"), + _("Check if object resources have finished to load in background."), + _("Object _PARAM1_ was preloaded in background"), + "", + "res/actions/hourglass_black.svg", + "res/actions/hourglass_black.svg") + .SetHelpPath("/all-features/resources-loading") + .AddCodeOnlyParameter("currentScene", "") + .AddParameter("string", _("Object name")) + .MarkAsAdvanced(); } } // namespace gd diff --git a/Core/GDCore/IDE/Project/ArbitraryObjectsWorker.h b/Core/GDCore/IDE/Project/ArbitraryObjectsWorker.h index c289016e6..c6fc8bfd0 100644 --- a/Core/GDCore/IDE/Project/ArbitraryObjectsWorker.h +++ b/Core/GDCore/IDE/Project/ArbitraryObjectsWorker.h @@ -4,8 +4,7 @@ * reserved. This project is released under the MIT License. */ -#ifndef ARBITRARYOBJECTSWORKER_H -#define ARBITRARYOBJECTSWORKER_H +#pragma once #include #include @@ -37,6 +36,11 @@ class GD_CORE_API ArbitraryObjectsWorker { */ void Launch(gd::ObjectsContainer& objects) { VisitObjectContainer(objects); }; + /** + * \brief Launch the worker on the specified object. + */ + void Launch(gd::Object& object) { VisitObject(object); }; + private: void VisitObjectContainer(gd::ObjectsContainer& objects); void VisitObject(gd::Object& object); @@ -60,4 +64,3 @@ class GD_CORE_API ArbitraryObjectsWorker { } // namespace gd -#endif // ARBITRARYOBJECTSWORKER_H diff --git a/Core/GDCore/IDE/Project/ArbitraryResourceWorker.cpp b/Core/GDCore/IDE/Project/ArbitraryResourceWorker.cpp index 0b7e8490b..a7acf9c70 100644 --- a/Core/GDCore/IDE/Project/ArbitraryResourceWorker.cpp +++ b/Core/GDCore/IDE/Project/ArbitraryResourceWorker.cpp @@ -291,6 +291,9 @@ GetResourceWorkerOnEvents(const gd::Project &project, } void ResourceWorkerInObjectsWorker::DoVisitObject(gd::Object &object) { + if (!shouldCheckObject(object)) { + return; + } object.GetConfiguration().ExposeResources(worker); auto& effects = object.GetEffects(); for (size_t effectIndex = 0; effectIndex < effects.GetEffectsCount(); effectIndex++) @@ -307,7 +310,16 @@ void ResourceWorkerInObjectsWorker::DoVisitBehavior(gd::Behavior &behavior){ gd::ResourceWorkerInObjectsWorker GetResourceWorkerOnObjects(const gd::Project &project, gd::ArbitraryResourceWorker &worker) { - gd::ResourceWorkerInObjectsWorker resourcesWorker(project, worker); + std::function shouldCheckObject = + [](const gd::Object &object) { return true; }; + return GetResourceWorkerOnObjects(project, worker, shouldCheckObject); +} + +gd::ResourceWorkerInObjectsWorker GetResourceWorkerOnObjects( + const gd::Project &project, gd::ArbitraryResourceWorker &worker, + std::function &shouldCheckObject) { + gd::ResourceWorkerInObjectsWorker resourcesWorker(project, worker, + shouldCheckObject); return resourcesWorker; } diff --git a/Core/GDCore/IDE/Project/ArbitraryResourceWorker.h b/Core/GDCore/IDE/Project/ArbitraryResourceWorker.h index e037fc887..041d4f8f9 100644 --- a/Core/GDCore/IDE/Project/ArbitraryResourceWorker.h +++ b/Core/GDCore/IDE/Project/ArbitraryResourceWorker.h @@ -178,8 +178,11 @@ ResourceWorkerInEventsWorker GD_CORE_API GetResourceWorkerOnEvents( class GD_CORE_API ResourceWorkerInObjectsWorker : public gd::ArbitraryObjectsWorker { public: - ResourceWorkerInObjectsWorker(const gd::Project &project_, gd::ArbitraryResourceWorker &worker_) - : project(project_), worker(worker_){}; + ResourceWorkerInObjectsWorker( + const gd::Project &project_, gd::ArbitraryResourceWorker &worker_, + std::function &shouldCheckObject_) + : project(project_), worker(worker_), + shouldCheckObject(shouldCheckObject_){}; ~ResourceWorkerInObjectsWorker() {} private: @@ -188,9 +191,14 @@ private: const gd::Project &project; gd::ArbitraryResourceWorker &worker; + std::function shouldCheckObject; }; -gd::ResourceWorkerInObjectsWorker GD_CORE_API -GetResourceWorkerOnObjects(const gd::Project &project, gd::ArbitraryResourceWorker &worker); +gd::ResourceWorkerInObjectsWorker GD_CORE_API GetResourceWorkerOnObjects( + const gd::Project &project, gd::ArbitraryResourceWorker &worker); + +gd::ResourceWorkerInObjectsWorker GD_CORE_API GetResourceWorkerOnObjects( + const gd::Project &project, gd::ArbitraryResourceWorker &worker, + std::function &shouldCheckObject); } // namespace gd diff --git a/Core/GDCore/IDE/Project/SceneResourcesFinder.cpp b/Core/GDCore/IDE/Project/SceneResourcesFinder.cpp index fc6b14deb..9d8f8cf94 100644 --- a/Core/GDCore/IDE/Project/SceneResourcesFinder.cpp +++ b/Core/GDCore/IDE/Project/SceneResourcesFinder.cpp @@ -9,6 +9,7 @@ #include "GDCore/Project/EventsBasedObjectVariant.h" #include "GDCore/Project/Layout.h" #include "GDCore/Project/Project.h" +#include "GDCore/Project/Object.h" #include "GDCore/Serialization/SerializerElement.h" namespace gd { @@ -20,11 +21,29 @@ std::set SceneResourcesFinder::FindProjectResources(gd::Project &pro return resourceWorker.resourceNames; } -std::set SceneResourcesFinder::FindSceneResources(gd::Project &project, - gd::Layout &layout) { +std::set +SceneResourcesFinder::FindSceneResources(gd::Project &project, + gd::Layout &layout, + bool ignoreObjectResourcePreloading) { gd::SceneResourcesFinder resourceWorker(project.GetResourcesManager()); - gd::ResourceExposer::ExposeLayoutResources(project, layout, resourceWorker); + std::function shouldCheckObject = + [ignoreObjectResourcePreloading](const gd::Object &object) { + return ignoreObjectResourcePreloading || + object.GetResourcesPreloading() != "manually"; + }; + gd::ResourceExposer::ExposeLayoutResources(project, layout, resourceWorker, + shouldCheckObject); + return resourceWorker.resourceNames; +} + +std::set +SceneResourcesFinder::FindObjectResources(gd::Project &project, + gd::Object &object) { + gd::SceneResourcesFinder resourceWorker(project.GetResourcesManager()); + + auto objectWorker = gd::GetResourceWorkerOnObjects(project, resourceWorker); + objectWorker.Launch(object); return resourceWorker.resourceNames; } diff --git a/Core/GDCore/IDE/Project/SceneResourcesFinder.h b/Core/GDCore/IDE/Project/SceneResourcesFinder.h index ea3db9512..e82091b23 100644 --- a/Core/GDCore/IDE/Project/SceneResourcesFinder.h +++ b/Core/GDCore/IDE/Project/SceneResourcesFinder.h @@ -32,8 +32,9 @@ public: * * It doesn't include resources used globally. */ - static std::set FindSceneResources(gd::Project &project, - gd::Layout &layout); + static std::set + FindSceneResources(gd::Project &project, gd::Layout &layout, + bool ignoreObjectResourcePreloading); /** * @brief Find resource that are used globally in the project. @@ -42,6 +43,12 @@ public: */ static std::set FindProjectResources(gd::Project &project); + /** + * @brief Find resource usages in a given object. + */ + static std::set FindObjectResources(gd::Project &project, + gd::Object &object); + /** * @brief Find resource usages in a given events-based object variant. */ diff --git a/Core/GDCore/IDE/ProjectStripper.cpp b/Core/GDCore/IDE/ProjectStripper.cpp index 82d094da3..d13cd8326 100644 --- a/Core/GDCore/IDE/ProjectStripper.cpp +++ b/Core/GDCore/IDE/ProjectStripper.cpp @@ -26,7 +26,6 @@ void GD_CORE_API ProjectStripper::StripProjectForExport(gd::Project &project) { wholeProjectBrowser.ExposeObjects(project, behaviorDefaultFlagClearer); for (unsigned int i = 0; i < project.GetLayoutsCount(); ++i) { - project.GetLayout(i).GetObjects().GetObjectGroups().Clear(); project.GetLayout(i).GetEvents().Clear(); } diff --git a/Core/GDCore/IDE/ResourceExposer.cpp b/Core/GDCore/IDE/ResourceExposer.cpp index 4c47c1f49..a037bb205 100644 --- a/Core/GDCore/IDE/ResourceExposer.cpp +++ b/Core/GDCore/IDE/ResourceExposer.cpp @@ -15,6 +15,7 @@ #include "GDCore/Project/EventsFunctionsExtension.h" #include "GDCore/Project/Layout.h" #include "GDCore/Project/Project.h" +#include "GDCore/Project/Object.h" #include "GDCore/String.h" namespace gd { @@ -89,11 +90,20 @@ void ResourceExposer::ExposeProjectResources( } void ResourceExposer::ExposeLayoutResources( - gd::Project &project, - gd::Layout &layout, + gd::Project &project, gd::Layout &layout, gd::ArbitraryResourceWorker &worker) { + std::function shouldCheckObject = + [](const gd::Object &object) { return true; }; + gd::ResourceExposer::ExposeLayoutResources(project, layout, worker, + shouldCheckObject); +} + +void ResourceExposer::ExposeLayoutResources( + gd::Project &project, gd::Layout &layout, + gd::ArbitraryResourceWorker &worker, + std::function &shouldCheckObject) { // Expose object configuration resources - auto objectWorker = gd::GetResourceWorkerOnObjects(project, worker); + auto objectWorker = gd::GetResourceWorkerOnObjects(project, worker, shouldCheckObject); gd::ProjectBrowserHelper::ExposeLayoutObjects(layout, objectWorker); // Expose layer effect resources diff --git a/Core/GDCore/IDE/ResourceExposer.h b/Core/GDCore/IDE/ResourceExposer.h index 14b65482f..01407f3a1 100644 --- a/Core/GDCore/IDE/ResourceExposer.h +++ b/Core/GDCore/IDE/ResourceExposer.h @@ -5,6 +5,8 @@ */ #pragma once +#include + namespace gd { class Platform; class Project; @@ -13,6 +15,7 @@ class EventsBasedObjectVariant; class EventsFunctionsExtension; class Effect; class Layout; +class Object; } // namespace gd namespace gd { @@ -47,10 +50,19 @@ public: * * It doesn't include resources used globally. */ - static void ExposeLayoutResources(gd::Project &project, - gd::Layout &layout, + static void ExposeLayoutResources(gd::Project &project, gd::Layout &layout, gd::ArbitraryResourceWorker &worker); + /** + * @brief Expose the resources used in a given layout. + * + * It doesn't include resources used globally. + */ + static void ExposeLayoutResources( + gd::Project &project, gd::Layout &layout, + gd::ArbitraryResourceWorker &worker, + std::function &shouldCheckObject); + /** * @brief Expose the resources used in a given events-based object variant. */ diff --git a/Core/GDCore/Project/Object.cpp b/Core/GDCore/Project/Object.cpp index c7b55366c..60420e926 100644 --- a/Core/GDCore/Project/Object.cpp +++ b/Core/GDCore/Project/Object.cpp @@ -52,6 +52,7 @@ void Object::CopyWithoutConfiguration(const gd::Object& object) { objectVariables = object.objectVariables; effectsContainer = object.effectsContainer; behaviors = object.behaviors; + resourcesPreloading = object.resourcesPreloading; } gd::ObjectConfiguration& Object::GetConfiguration() { return *configuration; } @@ -95,6 +96,7 @@ void Object::UnserializeFrom(gd::Project& project, SetType(element.GetStringAttribute("type")); assetStoreId = element.GetStringAttribute("assetStoreId"); name = element.GetStringAttribute("name", name, "nom"); + resourcesPreloading = element.GetStringAttribute("resourcesPreloading", "with-scene"); objectVariables.UnserializeFrom( element.GetChild("variables", 0, "Variables")); @@ -134,6 +136,9 @@ void Object::SerializeTo(SerializerElement& element) const { element.SetAttribute("name", GetName()); element.SetAttribute("assetStoreId", GetAssetStoreId()); element.SetAttribute("type", GetType()); + if (GetResourcesPreloading() != "with-scene") { + element.SetAttribute("resourcesPreloading", GetResourcesPreloading()); + } objectVariables.SerializeTo(element.AddChild("variables")); effectsContainer.SerializeTo(element.AddChild("effects")); behaviors.SerializeTo(element.AddChild("behaviors")); diff --git a/Core/GDCore/Project/Object.h b/Core/GDCore/Project/Object.h index 95c2c69b0..e66fd7b41 100644 --- a/Core/GDCore/Project/Object.h +++ b/Core/GDCore/Project/Object.h @@ -124,6 +124,22 @@ class GD_CORE_API Object { */ const gd::String& GetType() const { return configuration->GetType(); } + /** + * Set when the object resources must be preloaded: `with-scene`(default), + * `manually`. + */ + void SetResourcesPreloading(gd::String resourcesPreloading_) { + resourcesPreloading = resourcesPreloading_; + } + + /** + * Get when the object resources must be preloaded: `with-scene`(default), + * `manually`. + */ + const gd::String& GetResourcesPreloading() const { + return resourcesPreloading; + } + ///@} /** \name Behaviors management @@ -267,6 +283,8 @@ class GD_CORE_API Object { effectsContainer; ///< The effects container for the object. mutable gd::String persistentUuid; ///< A persistent random version 4 UUID, ///< useful for computing changesets. + /** When set to `"manually"`, its resources are not preloaded with the scene. */ + gd::String resourcesPreloading = "with-scene"; /** * Initialize object using another object. Used by copy-ctor and assign-op. diff --git a/Core/GDCore/Project/Project.h b/Core/GDCore/Project/Project.h index 8d8a6b576..95ae2685e 100644 --- a/Core/GDCore/Project/Project.h +++ b/Core/GDCore/Project/Project.h @@ -996,16 +996,16 @@ class GD_CORE_API Project { ResourcesContainer& GetResourcesManager() { return resourcesContainer; } /** - * Set when the scenes must preload their resources: `at-startup`, `never` - * (default). + * Set when the scenes must preload their resources: `at-startup` (default), + * `never`. */ void SetSceneResourcesPreloading(gd::String sceneResourcesPreloading_) { sceneResourcesPreloading = sceneResourcesPreloading_; } /** - * Get when the scenes must preload their resources: `at-startup`, `never` - * (default). + * Get when the scenes must preload their resources: `at-startup` (default), + * `never`. */ const gd::String& GetSceneResourcesPreloading() const { return sceneResourcesPreloading; diff --git a/Extensions/AnchorBehavior/tests/anchorruntimebehavior.spec.js b/Extensions/AnchorBehavior/tests/anchorruntimebehavior.spec.js index 9955314fb..0ec528d1d 100644 --- a/Extensions/AnchorBehavior/tests/anchorruntimebehavior.spec.js +++ b/Extensions/AnchorBehavior/tests/anchorruntimebehavior.spec.js @@ -38,6 +38,7 @@ describe('gdjs.AnchorRuntimeBehavior', () => { title: '', behaviorsSharedData: [], objects: [], + objectsGroups: [], instances: [], usedResources: [], uiSettings: { diff --git a/Extensions/DraggableBehavior/tests/draggableruntimebehavior.spec.js b/Extensions/DraggableBehavior/tests/draggableruntimebehavior.spec.js index 5d030d6c5..a16257ab5 100644 --- a/Extensions/DraggableBehavior/tests/draggableruntimebehavior.spec.js +++ b/Extensions/DraggableBehavior/tests/draggableruntimebehavior.spec.js @@ -27,6 +27,7 @@ describe('gdjs.DraggableRuntimeBehavior', function () { title: '', behaviorsSharedData: [], objects: [], + objectsGroups: [], instances: [], usedResources: [], uiSettings: { diff --git a/Extensions/LinkedObjects/tests/linkedobjects.spec.js b/Extensions/LinkedObjects/tests/linkedobjects.spec.js index 9a07fa7c4..35582e7be 100644 --- a/Extensions/LinkedObjects/tests/linkedobjects.spec.js +++ b/Extensions/LinkedObjects/tests/linkedobjects.spec.js @@ -26,6 +26,7 @@ describe('gdjs.LinksManager', function () { variables: [], behaviorsSharedData: [], objects: [], + objectsGroups: [], instances: [], r: 0, v: 0, diff --git a/Extensions/PathfindingBehavior/tests/FrameRatePathfindingRuntimeBehavior.spec.js b/Extensions/PathfindingBehavior/tests/FrameRatePathfindingRuntimeBehavior.spec.js index c96a140fd..9ad3251cf 100644 --- a/Extensions/PathfindingBehavior/tests/FrameRatePathfindingRuntimeBehavior.spec.js +++ b/Extensions/PathfindingBehavior/tests/FrameRatePathfindingRuntimeBehavior.spec.js @@ -34,6 +34,7 @@ describe('gdjs.PathfindingRuntimeBehavior', function () { title: '', behaviorsSharedData: [], objects: [], + objectsGroups: [], instances: [], usedResources: [], uiSettings: { diff --git a/Extensions/PathfindingBehavior/tests/commonpathfindingruntimebehavior.spec.js b/Extensions/PathfindingBehavior/tests/commonpathfindingruntimebehavior.spec.js index 3ca6ff37c..626d1f9b0 100644 --- a/Extensions/PathfindingBehavior/tests/commonpathfindingruntimebehavior.spec.js +++ b/Extensions/PathfindingBehavior/tests/commonpathfindingruntimebehavior.spec.js @@ -37,6 +37,7 @@ describe('gdjs.PathfindingRuntimeBehavior', function () { title: '', behaviorsSharedData: [], objects: [], + objectsGroups: [], instances: [], usedResources: [], uiSettings: { diff --git a/Extensions/PathfindingBehavior/tests/legacypathfindingruntimebehavior.spec.js b/Extensions/PathfindingBehavior/tests/legacypathfindingruntimebehavior.spec.js index ba1aabdf1..fe5396c9c 100644 --- a/Extensions/PathfindingBehavior/tests/legacypathfindingruntimebehavior.spec.js +++ b/Extensions/PathfindingBehavior/tests/legacypathfindingruntimebehavior.spec.js @@ -39,6 +39,7 @@ describe('gdjs.PathfindingRuntimeBehavior', function () { title: '', behaviorsSharedData: [], objects: [], + objectsGroups: [], instances: [], usedResources: [], uiSettings: { diff --git a/Extensions/PrimitiveDrawing/tests/shapepainterruntimeobject.pixiruntimegamewithassets.spec.js b/Extensions/PrimitiveDrawing/tests/shapepainterruntimeobject.pixiruntimegamewithassets.spec.js index 1c6197848..f59697309 100644 --- a/Extensions/PrimitiveDrawing/tests/shapepainterruntimeobject.pixiruntimegamewithassets.spec.js +++ b/Extensions/PrimitiveDrawing/tests/shapepainterruntimeobject.pixiruntimegamewithassets.spec.js @@ -47,6 +47,7 @@ describe('gdjs.ShapePainterRuntimeObject (using a PixiJS RuntimeGame with assets title: '', behaviorsSharedData: [], objects: [], + objectsGroups: [], instances: [], variables: [], usedResources: [], diff --git a/Extensions/SaveState/tests/SaveState.spec.js b/Extensions/SaveState/tests/SaveState.spec.js index b55025d2f..55e4fd012 100644 --- a/Extensions/SaveState/tests/SaveState.spec.js +++ b/Extensions/SaveState/tests/SaveState.spec.js @@ -68,6 +68,7 @@ describe('SaveState', () => { updateIfNotVisible: false, }, ], + objectsGroups: [], instances: instances || [], variables: [], usedResources: [], diff --git a/Extensions/TextInput/tests/textinputruntimeobject.pixiruntimegamewithassets.spec.js b/Extensions/TextInput/tests/textinputruntimeobject.pixiruntimegamewithassets.spec.js index 49a39f477..99299e2e0 100644 --- a/Extensions/TextInput/tests/textinputruntimeobject.pixiruntimegamewithassets.spec.js +++ b/Extensions/TextInput/tests/textinputruntimeobject.pixiruntimegamewithassets.spec.js @@ -58,6 +58,7 @@ describe('gdjs.TextInputRuntimeObject (using a PixiJS RuntimeGame with DOM eleme title: '', behaviorsSharedData: [], objects: [], + objectsGroups: [], instances: [], variables: [], usedResources: [], diff --git a/Extensions/TileMap/tests/tilemapcollisionmaskruntimeobject.spec.js b/Extensions/TileMap/tests/tilemapcollisionmaskruntimeobject.spec.js index 3aaa06c10..f3dba783a 100644 --- a/Extensions/TileMap/tests/tilemapcollisionmaskruntimeobject.spec.js +++ b/Extensions/TileMap/tests/tilemapcollisionmaskruntimeobject.spec.js @@ -55,6 +55,7 @@ describe('gdjs.TileMapCollisionMaskRuntimeObject', function () { title: '', behaviorsSharedData: [], objects: [], + objectsGroups: [], instances: [], usedResources: [], uiSettings: { diff --git a/Extensions/TopDownMovementBehavior/tests/topdownmovementbehavior.spec.js b/Extensions/TopDownMovementBehavior/tests/topdownmovementbehavior.spec.js index abaab93dd..c627f8a9f 100644 --- a/Extensions/TopDownMovementBehavior/tests/topdownmovementbehavior.spec.js +++ b/Extensions/TopDownMovementBehavior/tests/topdownmovementbehavior.spec.js @@ -32,6 +32,7 @@ describe('gdjs.TopDownMovementRuntimeBehavior', function () { title: '', behaviorsSharedData: [], objects: [], + objectsGroups: [], instances: [], usedResources: [], uiSettings: { diff --git a/GDJS/GDJS/Extensions/Builtin/SceneExtension.cpp b/GDJS/GDJS/Extensions/Builtin/SceneExtension.cpp index a1619e3c7..7c8093407 100644 --- a/GDJS/GDJS/Extensions/Builtin/SceneExtension.cpp +++ b/GDJS/GDJS/Extensions/Builtin/SceneExtension.cpp @@ -51,6 +51,12 @@ SceneExtension::SceneExtension() { "gdjs.evtTools.runtimeScene.getSceneLoadingProgress"); GetAllExpressions()["SceneLoadingProgress"].SetFunctionName( "gdjs.evtTools.runtimeScene.getSceneLoadingProgress"); + GetAllActions()["LoadObjectAssets"].SetFunctionName( + "gdjs.evtTools.runtimeScene.loadObjectOrGroupAssets"); + GetAllActions()["UnloadObjectAssets"].SetFunctionName( + "gdjs.evtTools.runtimeScene.unloadObjectOrGroupAssets"); + GetAllConditions()["AreObjectAssetsLoaded"].SetFunctionName( + "gdjs.evtTools.runtimeScene.areObjectOrGroupAssetsLoaded"); StripUnimplementedInstructionsAndExpressions(); } diff --git a/GDJS/GDJS/IDE/ExporterHelper.cpp b/GDJS/GDJS/IDE/ExporterHelper.cpp index cd918eeaa..493d222eb 100644 --- a/GDJS/GDJS/IDE/ExporterHelper.cpp +++ b/GDJS/GDJS/IDE/ExporterHelper.cpp @@ -578,7 +578,8 @@ void ExporterHelper::StripAndSerializeProjectData( layoutIndex++) { auto &layout = project.GetLayout(layoutIndex); auto sceneUsedResources = gd::SceneResourcesFinder::FindSceneResources( - project, layout); + project, layout, + /* ignoreObjectResourcePreloading= */ isInGameEdition); for (auto &&resourceName : projectUsedResources) { sceneUsedResources.erase(resourceName); } @@ -619,9 +620,11 @@ void ExporterHelper::StripAndSerializeProjectData( gd::ProjectStripper::StripProjectForExport(project); project.SerializeTo(rootElement); - SerializeUsedResources(rootElement, projectUsedResources, scenesUsedResources, - eventsBasedObjectVariantsUsedResources); + SerializeUsedResourcesForRuntime(project, rootElement, projectUsedResources, + scenesUsedResources); if (isInGameEdition) { + SerializeUsedResourcesForInGameEditor( + project, rootElement, eventsBasedObjectVariantsUsedResources); auto &behaviorsElement = rootElement.AddChild("activatedByDefaultInEditorBehaviors"); behaviorsElement.ConsiderAsArrayOf("resourceReference"); auto &platform = project.GetCurrentPlatform(); @@ -638,23 +641,22 @@ void ExporterHelper::StripAndSerializeProjectData( } void ExporterHelper::SerializeUsedResources( + gd::SerializerElement &element, std::set &usedResources) { + auto &resourcesElement = element.AddChild("usedResources"); + resourcesElement.ConsiderAsArrayOf("resourceReference"); + for (auto &resourceName : usedResources) { + auto &resourceElement = resourcesElement.AddChild("resourceReference"); + resourceElement.SetAttribute("name", resourceName); + } +} + +void ExporterHelper::SerializeUsedResourcesForRuntime( + gd::Project &project, gd::SerializerElement &rootElement, std::set &projectUsedResources, - std::unordered_map> &scenesUsedResources, - std::unordered_map> - &eventsBasedObjectVariantsUsedResources) { - auto serializeUsedResources = - [](gd::SerializerElement &element, - std::set &usedResources) -> void { - auto &resourcesElement = element.AddChild("usedResources"); - resourcesElement.ConsiderAsArrayOf("resourceReference"); - for (auto &resourceName : usedResources) { - auto &resourceElement = resourcesElement.AddChild("resourceReference"); - resourceElement.SetAttribute("name", resourceName); - } - }; + std::unordered_map> &scenesUsedResources) { - serializeUsedResources(rootElement, projectUsedResources); + SerializeUsedResources(rootElement, projectUsedResources); auto &layoutsElement = rootElement.GetChild("layouts"); for (std::size_t layoutIndex = 0; @@ -663,10 +665,36 @@ void ExporterHelper::SerializeUsedResources( auto &layoutElement = layoutsElement.GetChild(layoutIndex); const auto layoutName = layoutElement.GetStringAttribute("name"); - auto &layoutUsedResources = scenesUsedResources[layoutName]; - serializeUsedResources(layoutElement, layoutUsedResources); - } + auto &sceneUsedResources = scenesUsedResources[layoutName]; + SerializeUsedResources(layoutElement, sceneUsedResources); + auto &scene = project.GetLayout(layoutName); + auto &objectsElement = layoutElement.GetChild("objects"); + for (std::size_t objectIndex = 0; + objectIndex < objectsElement.GetChildrenCount(); objectIndex++) { + auto &objectElement = objectsElement.GetChild(objectIndex); + const auto objectName = objectElement.GetStringAttribute("name"); + + auto &object = scene.GetObjects().GetObject(objectName); + if (object.GetResourcesPreloading() == "manually") { + auto objectUsedResources = + gd::SceneResourcesFinder::FindObjectResources(project, object); + for (auto &&resourceName : projectUsedResources) { + objectUsedResources.erase(resourceName); + } + for (auto &&resourceName : sceneUsedResources) { + objectUsedResources.erase(resourceName); + } + SerializeUsedResources(objectElement, objectUsedResources); + } + } + } +} + +void ExporterHelper::SerializeUsedResourcesForInGameEditor( + gd::Project &project, gd::SerializerElement &rootElement, + std::unordered_map> + &eventsBasedObjectVariantsUsedResources) { auto &extensionsElement = rootElement.GetChild("eventsFunctionsExtensions"); for (std::size_t extensionIndex = 0; extensionIndex < extensionsElement.GetChildrenCount(); @@ -685,7 +713,7 @@ void ExporterHelper::SerializeUsedResources( gd::PlatformExtension::GetObjectFullType(extensionName, objectName); auto &objectUsedResources = eventsBasedObjectVariantsUsedResources[eventsBasedObjectType]; - serializeUsedResources(objectElement, objectUsedResources); + SerializeUsedResources(objectElement, objectUsedResources); auto &variantsElement = objectElement.GetChild("variants"); for (std::size_t variantIndex = 0; @@ -697,7 +725,7 @@ void ExporterHelper::SerializeUsedResources( extensionName, objectName, variantName); auto &variantUsedResources = eventsBasedObjectVariantsUsedResources[variantType]; - serializeUsedResources(variantElement, variantUsedResources); + SerializeUsedResources(variantElement, variantUsedResources); } } } diff --git a/GDJS/GDJS/IDE/ExporterHelper.h b/GDJS/GDJS/IDE/ExporterHelper.h index 709422b1c..17bbb97e0 100644 --- a/GDJS/GDJS/IDE/ExporterHelper.h +++ b/GDJS/GDJS/IDE/ExporterHelper.h @@ -751,14 +751,19 @@ class ExporterHelper { ///< be then copied to the final output directory. private: - static void - SerializeUsedResources(gd::SerializerElement &rootElement, - std::set &projectUsedResources, - std::unordered_map> - &layersUsedResources, - std::unordered_map> - &eventsBasedObjectVariantsUsedResources); + static void SerializeUsedResourcesForRuntime( + gd::Project &project, gd::SerializerElement &rootElement, + std::set &projectUsedResources, + std::unordered_map> + &layersUsedResources); + static void SerializeUsedResourcesForInGameEditor( + gd::Project &project, gd::SerializerElement &rootElement, + std::unordered_map> + &eventsBasedObjectVariantsUsedResources); + + static void SerializeUsedResources(gd::SerializerElement &element, + std::set &usedResources); /** * \brief Strip a project and serialize it to JSON. */ diff --git a/GDJS/Runtime/ResourceLoader.ts b/GDJS/Runtime/ResourceLoader.ts index d7513afcd..66d8ed777 100644 --- a/GDJS/Runtime/ResourceLoader.ts +++ b/GDJS/Runtime/ResourceLoader.ts @@ -5,7 +5,8 @@ */ namespace gdjs { const logger = new gdjs.Logger('ResourceLoader'); - const debugLogger = new gdjs.Logger('ResourceLoader - debug').enable(false); + // TODO add a condition before each log to avoid building the message for nothing. + const debugLogger = new gdjs.Logger('ResourceLoader - debug').enable(true); const addSearchParameterToUrl = ( url: string, @@ -36,14 +37,14 @@ namespace gdjs { * and make impossible to finely tune in which order scenes are actually * downloaded. */ - class SceneLoadingTask { - sceneName: string; + class LoadingTask { + identifier: string; private onProgressCallbacks: Array<(count: number, total: number) => void>; private onFinishCallbacks: Array<() => void>; private isFinished = false; - constructor(sceneName: string) { - this.sceneName = sceneName; + constructor(identifier: string) { + this.identifier = identifier; this.onProgressCallbacks = new Array< (count: number, total: number) => void >(); @@ -107,6 +108,16 @@ namespace gdjs { errors: Array>; }; + type LoadingTaskState = { + resourceNames: Array; + /** + * - `'not-loaded'` Resources are not loaded. + * - `'loaded'` Resources are loaded but not parsed. + * - `'ready'` Resources are loaded and parsed. + */ + status: 'not-loaded' | 'loaded' | 'ready'; + }; + /** * Pre-load resources of any kind needed for a game or a scene. * @category Resources @@ -117,30 +128,11 @@ namespace gdjs { static maxAttempt = 3; _runtimeGame: RuntimeGame; - /** - * All the resource of a game by resource name. - */ - private _resources: Map; /** * Resources needed for any scene. Typically, they are resources from * global objects. */ private _globalResources: Array; - /** - * Resources and the loading state of each scene, indexed by scene name. - */ - private _sceneLoadingStates: Map< - string, - { - resourceNames: Array; - status: 'not-loaded' | 'loaded' | 'ready'; - } - > = new Map(); - /** - * A queue of scenes whose resources are still to be pre-loaded. - */ - private _sceneToLoadQueue: Array = - new Array(); /** * The resource managers that actually download and remember downloaded * content. @@ -156,21 +148,28 @@ namespace gdjs { private _spineManager: SpineManager | null = null; private _svgManager: InternalInGameEditorOnlySvgManager; - /** - * The name of the scene for which resources are currently being loaded. - */ - private currentLoadingSceneName: string = ''; - /** - * The progress, between 0 and 1, of the loading of the resource, for the - * scene that is being loaded (see `currentLoadingSceneName`). - */ - private currentSceneLoadingProgress: float = 0; - /** - * It's set to `true` during intermediary loading screen to use a greater - * concurrency as the game is paused and doesn't need bandwidth (for video - * or music streaming or online multiplayer). - */ - private _isLoadingInForeground = true; + private privateResourceManager = new PrivateResourceManager(this); + private sceneResourceLoadingQueue = new ResourceLoadingQueue( + 'scene', + this.privateResourceManager, + /** shouldProcessResources= */ false, + ( + unloadedTaskIdentifier, + unloadedTaskState, + newTaskIdentifier, + newTaskState + ) => + this._getResourcesOnlyUsedInUnloadedScene( + unloadedTaskIdentifier, + unloadedTaskState, + newTaskIdentifier, + newTaskState + ) + ); + private objectResourceLoadingQueues = new Map< + string, + ResourceLoadingQueue + >(); /** * @param runtimeGame The game. @@ -185,7 +184,6 @@ namespace gdjs { layoutDataArray: Array ) { this._runtimeGame = runtimeGame; - this._resources = new Map(); this._globalResources = globalResources; // These 3 attributes are filled by `setResources`. @@ -241,19 +239,18 @@ namespace gdjs { ): void { this._globalResources = globalResources; - this._sceneLoadingStates.clear(); + // TODO We should probably instanciate new queues to avoid side effects from running tasks. + this.sceneResourceLoadingQueue.clear(); + for (const objectResourceLoadingQueue of this.objectResourceLoadingQueues.values()) { + objectResourceLoadingQueue.clear(); + } for (const layoutData of layoutDataArray) { - this._sceneLoadingStates.set(layoutData.name, { - resourceNames: layoutData.usedResources.map( - (resource) => resource.name - ), - status: 'not-loaded', - }); + this.sceneResourceLoadingQueue.registerResources( + layoutData.name, + layoutData.usedResources + ); } - // TODO Clearing the queue doesn't abort the running task, but it should - // not matter as resource loading is really fast in preview mode. - this._sceneToLoadQueue.length = 0; for (let index = layoutDataArray.length - 1; index >= 0; index--) { const layoutData = layoutDataArray[index]; @@ -264,11 +261,11 @@ namespace gdjs { : resourcesPreloading; if (resolvedResourcesPreloading === 'at-startup') { - this._sceneToLoadQueue.push(new SceneLoadingTask(layoutData.name)); + this.sceneResourceLoadingQueue.enqueue(layoutData.name); } } - this._resources.clear(); + this.privateResourceManager._resources.clear(); for (const resourceData of resourceDataArray) { if (!resourceData.file) { // Empty string or missing `file` field: not a valid resource, let's entirely ignore it. @@ -277,7 +274,10 @@ namespace gdjs { continue; } - this._resources.set(resourceData.name, resourceData); + this.privateResourceManager._resources.set( + resourceData.name, + resourceData + ); } } @@ -286,19 +286,20 @@ namespace gdjs { ): Promise { let loadedCount = 0; await ResourceLoader.processAndRetryIfNeededWithPromisePool( - [...this._resources.values()], + [...this.privateResourceManager._resources.values()], ResourceLoader.maxForegroundConcurrency, ResourceLoader.maxAttempt, async (resource) => { - await this._loadResource(resource); - await this._processResource(resource); + await this.privateResourceManager._loadResource(resource); + await this.privateResourceManager._processResource(resource); loadedCount++; - onProgress(loadedCount, this._resources.size); + onProgress(loadedCount, this.privateResourceManager._resources.size); } ); - for (const sceneLoadingState of this._sceneLoadingStates.values()) { - sceneLoadingState.status = 'ready'; + this.sceneResourceLoadingQueue.setAllResourcesAs('ready'); + for (const objectResourceLoadingQueue of this.objectResourceLoadingQueues.values()) { + objectResourceLoadingQueue.setAllResourcesAs('ready'); } } @@ -312,13 +313,14 @@ namespace gdjs { ResourceLoader.maxForegroundConcurrency, ResourceLoader.maxAttempt, async (resourceName) => { - const resource = this._resources.get(resourceName); + const resource = + this.privateResourceManager._resources.get(resourceName); if (resource) { - await this._loadResource(resource); - await this._processResource(resource); + await this.privateResourceManager._loadResource(resource); + await this.privateResourceManager._processResource(resource); } loadedCount++; - onProgress(loadedCount, this._resources.size); + onProgress(loadedCount, this.privateResourceManager._resources.size); } ); } @@ -330,8 +332,9 @@ namespace gdjs { firstSceneName: string, onProgress: (count: number, total: number) => void ): Promise { - const firstSceneState = this._sceneLoadingStates.get(firstSceneName); - if (!firstSceneState) { + const firstSceneResourceNames = + this.sceneResourceLoadingQueue.getResourceNamesFor(firstSceneName); + if (!firstSceneResourceNames) { logger.warn( 'Can\'t load resource for unknown scene: "' + firstSceneName + '".' ); @@ -341,26 +344,27 @@ namespace gdjs { let loadedCount = 0; const resourceNames = [ ...this._globalResources, - ...firstSceneState.resourceNames, + ...firstSceneResourceNames, ]; await ResourceLoader.processAndRetryIfNeededWithPromisePool( resourceNames, ResourceLoader.maxForegroundConcurrency, ResourceLoader.maxAttempt, async (resourceName) => { - const resource = this._resources.get(resourceName); + const resource = + this.privateResourceManager._resources.get(resourceName); if (!resource) { logger.warn('Unable to find resource "' + resourceName + '".'); return; } - await this._loadResource(resource); - await this._processResource(resource); + await this.privateResourceManager._loadResource(resource); + await this.privateResourceManager._processResource(resource); loadedCount++; onProgress(loadedCount, resourceNames.length); } ); - firstSceneState.status = 'ready'; + this.sceneResourceLoadingQueue.setResourcesAs(firstSceneName, 'ready'); } /** @@ -370,96 +374,7 @@ namespace gdjs { * scenes. */ async loadAllSceneInBackground(): Promise { - if (this.currentLoadingSceneName) { - return; - } - - debugLogger.log('Loading all scene resources, in background.'); - while (this._sceneToLoadQueue.length > 0) { - debugLogger.log( - `Still resources of ${this._sceneToLoadQueue.length} scene(s) to load: ${this._sceneToLoadQueue.map((task) => task.sceneName).join(', ')}` - ); - const task = this._sceneToLoadQueue[this._sceneToLoadQueue.length - 1]; - if (task === undefined) { - continue; - } - this.currentLoadingSceneName = task.sceneName; - if (!this.areSceneAssetsLoaded(task.sceneName)) { - debugLogger.log( - `Loading (but not processing) resources for scene ${task.sceneName}.` - ); - await this._doLoadSceneResources( - task.sceneName, - async (count, total) => task.onProgress(count, total) - ); - debugLogger.log( - `Done loading (but not processing) resources for scene ${task.sceneName}.` - ); - - // A scene may have been moved last while awaiting resources to be - // downloaded (see _prioritizeScene). - this._sceneToLoadQueue.splice( - this._sceneToLoadQueue.findIndex((element) => element === task), - 1 - ); - task.onFinish(); - } else { - this._sceneToLoadQueue.pop(); - } - } - debugLogger.log(`Scene resources loading finished.`); - this.currentLoadingSceneName = ''; - } - - private async _doLoadSceneResources( - sceneName: string, - onProgress?: (count: number, total: number) => Promise - ): Promise { - const sceneState = this._sceneLoadingStates.get(sceneName); - if (!sceneState) { - logger.warn( - 'Can\'t load resource for unknown scene: "' + sceneName + '".' - ); - return; - } - - let loadedCount = 0; - await ResourceLoader.processAndRetryIfNeededWithPromisePool( - sceneState.resourceNames, - this._isLoadingInForeground - ? ResourceLoader.maxForegroundConcurrency - : ResourceLoader.maxBackgroundConcurrency, - ResourceLoader.maxAttempt, - async (resourceName) => { - const resource = this._resources.get(resourceName); - if (!resource) { - logger.warn('Unable to find resource "' + resourceName + '".'); - return; - } - await this._loadResource(resource); - loadedCount++; - this.currentSceneLoadingProgress = - loadedCount / sceneState.resourceNames.length; - onProgress && - (await onProgress(loadedCount, sceneState.resourceNames.length)); - } - ); - sceneState.status = 'loaded'; - } - - private async _loadResource(resource: ResourceData): Promise { - const resourceManager = this._resourceManagersMap.get(resource.kind); - if (!resourceManager) { - logger.warn( - 'Unknown resource kind: "' + - resource.kind + - '" for: "' + - resource.name + - '".' - ); - return; - } - await resourceManager.loadResource(resource.name); + this.sceneResourceLoadingQueue.loadAllTasksInBackground(); } /** @@ -475,28 +390,10 @@ namespace gdjs { return; } await this.loadSceneResources(sceneName, onProgress); - - const sceneState = this._sceneLoadingStates.get(sceneName); - if (!sceneState) { - logger.warn( - 'Can\'t load resource for unknown scene: "' + sceneName + '".' - ); - return; - } - - let parsedCount = 0; - for (const resourceName of sceneState.resourceNames) { - const resource = this._resources.get(resourceName); - if (!resource) { - logger.warn('Unable to find resource "' + resourceName + '".'); - continue; - } - await this._processResource(resource); - parsedCount++; - onProgress && - (await onProgress(parsedCount, sceneState.resourceNames.length)); - } - sceneState.status = 'ready'; + await this.sceneResourceLoadingQueue.processResources( + sceneName, + onProgress + ); } /** @@ -514,11 +411,11 @@ namespace gdjs { `Prioritization of loading of resources for scene ${sceneName} was requested.` ); - this._isLoadingInForeground = true; - const task = this._prioritizeScene(sceneName); + this.sceneResourceLoadingQueue.isLoadingInForeground = true; + const task = this.sceneResourceLoadingQueue.prioritize(sceneName); return new Promise((resolve, reject) => { if (!task) { - this._isLoadingInForeground = false; + this.sceneResourceLoadingQueue.isLoadingInForeground = false; debugLogger.log( `Loading of resources for scene ${sceneName} was immediately resolved.` ); @@ -529,12 +426,73 @@ namespace gdjs { debugLogger.log( `Loading of resources for scene ${sceneName} just finished.` ); - this._isLoadingInForeground = false; + this.sceneResourceLoadingQueue.isLoadingInForeground = false; resolve(); }, onProgress); }); } + /** + * Preload an object assets in background. + */ + async loadObjectResources( + sceneName: string, + objectName: string, + usedResources: Array + ): Promise { + debugLogger.log( + `Loading of resources for object ${objectName} was requested.` + ); + const objectResourceLoadingQueue = + this.getObjectResourceLoadingQueue(sceneName); + objectResourceLoadingQueue.registerResources(objectName, usedResources); + const task = objectResourceLoadingQueue.enqueue(objectName); + objectResourceLoadingQueue.loadAllTasksInBackground(); + return new Promise((resolve, reject) => { + if (!task) { + debugLogger.log( + `Loading of resources for object ${objectName} was immediately resolved.` + ); + resolve(); + return; + } + task.registerCallback(() => { + debugLogger.log( + `Loading of resources for object ${objectName} just finished.` + ); + resolve(); + }); + }); + } + + private getObjectResourceLoadingQueue(sceneName: string) { + let objectResourceLoadingQueue = + this.objectResourceLoadingQueues.get(sceneName); + if (!objectResourceLoadingQueue) { + objectResourceLoadingQueue = new ResourceLoadingQueue( + `Independent objects of ${sceneName}`, + this.privateResourceManager, + /** shouldProcessResources= */ true, + ( + unloadedTaskIdentifier, + unloadedTaskState, + newTaskIdentifier, + newTaskState + ) => + this._getResourcesOnlyUsedInObject( + sceneName, + unloadedTaskIdentifier, + unloadedTaskState + ) + ); + this.objectResourceLoadingQueues.set( + sceneName, + objectResourceLoadingQueue + ); + } + return objectResourceLoadingQueue; + } + /** * To be called when the game is disposed. * Dispose all the resource managers. @@ -560,36 +518,47 @@ namespace gdjs { `Unloading of resources for scene ${unloadedSceneName} was requested.` ); - const sceneUniqueResourcesByKindMap = - this._getResourcesByKindOnlyUsedInUnloadedScene({ - unloadedSceneName, - newSceneName, - }); - - for (const [kindResourceManager, resourceManager] of this - ._resourceManagersMap) { - const resources = - sceneUniqueResourcesByKindMap.get(kindResourceManager); - if (resources) { - debugLogger.log( - `Unloading of resources of kind ${kindResourceManager} for scene ${unloadedSceneName}: `, - resources.map((resource) => resource.name).join(', ') - ); - for (const resource of resources) { - resourceManager.unloadResource(resource); - } - } - } + this.sceneResourceLoadingQueue.unloadResources( + unloadedSceneName, + newSceneName + ); + const objectResourceLoadingQueue = + this.getObjectResourceLoadingQueue(unloadedSceneName); + objectResourceLoadingQueue.clear(); debugLogger.log( `Unloading of resources for scene ${unloadedSceneName} finished.` ); + } - const sceneState = this._sceneLoadingStates.get(unloadedSceneName); - if (sceneState) { - sceneState.status = 'not-loaded'; + /** + * Unload an object assets in background. + */ + unloadObjectResources(sceneName: string, objectName: string): void { + const objectResourceLoadingQueue = + this.getObjectResourceLoadingQueue(sceneName); + if (!objectResourceLoadingQueue.areAssetsReady(objectName)) { + debugLogger.log( + `Can't unload of resources for object ${objectName} as it is not loaded.` + ); + return; } - // TODO: mark the scene as unloaded so it's not automatically loaded again eagerly. + for (const scene of this._runtimeGame._sceneStack.getAllScenes()) { + if (scene.getObjects(objectName).length > 0) { + debugLogger.log( + `Can't unload of resources for object ${objectName} as it still have instances living in the scene.` + ); + return; + } + } + debugLogger.log( + `Unloading of resources for object ${objectName} was requested.` + ); + objectResourceLoadingQueue.unloadResources(objectName); + objectResourceLoadingQueue.unregisterResources(objectName); + debugLogger.log( + `Unloading of resources for object ${objectName} finished.` + ); } /** @@ -597,84 +566,18 @@ namespace gdjs { */ unloadAllResources(): void { debugLogger.log(`Unloading of all resources was requested.`); - for (const resource of this._resources.values()) { - const resourceManager = this._resourceManagersMap.get(resource.kind); - if (resourceManager) { - resourceManager.unloadResource(resource); - } + for (const resource of this.privateResourceManager._resources.values()) { + this.privateResourceManager._unloadResource(resource.name); } - for (const sceneLoadingState of this._sceneLoadingStates.values()) { - sceneLoadingState.status = 'not-loaded'; + this.sceneResourceLoadingQueue.setAllResourcesAs('not-loaded'); + for (const objectResourceLoadingQueue of this.objectResourceLoadingQueues.values()) { + objectResourceLoadingQueue.clear(); } debugLogger.log(`Unloading of all resources finished.`); } - /** - * Put a given scene at the end of the queue. - * - * When the scene that is currently loading in background is done, - * this scene will be the next to be loaded. - */ - private _prioritizeScene(sceneName: string): SceneLoadingTask | null { - const sceneState = this._sceneLoadingStates.get(sceneName); - if (!sceneState) return null; - if (sceneState.status === 'loaded' || sceneState.status === 'ready') { - debugLogger.log( - `Scene ${sceneName} is already loaded. Skipping prioritization.` - ); - - // The scene is already loaded, nothing to do. - return null; - } - - // The scene is not loaded: either prioritize it or add it to the loading queue. - const taskIndex = this._sceneToLoadQueue.findIndex( - (task) => task.sceneName === sceneName - ); - let task: SceneLoadingTask; - if (taskIndex !== -1) { - // There is already a task for this scene in the queue. - // Move it so that it's loaded first. - task = this._sceneToLoadQueue[taskIndex]; - this._sceneToLoadQueue.splice(taskIndex, 1); - this._sceneToLoadQueue.push(task); - } else { - // There is no task for this scene in the queue. - // It might be because the scene was unloaded or never loaded. - // In this case, we need to add a new task to the queue. - task = new SceneLoadingTask(sceneName); - this._sceneToLoadQueue.push(task); - } - - // Re-start the loading process in the background. While at the beginning of the game - // it's not needed because already launched, a scene might be unloaded. This means - // that we then need to relaunch the loading process. - this.loadAllSceneInBackground(); - - return task; - } - - private async _processResource(resource: ResourceData): Promise { - const resourceManager = this._resourceManagersMap.get(resource.kind); - if (!resourceManager) { - logger.warn( - 'Unknown resource kind: "' + - resource.kind + - '" for: "' + - resource.name + - '".' - ); - return; - } - await resourceManager.processResource(resource.name); - } - getSceneLoadingProgress(sceneName: string): float { - return sceneName === this.currentLoadingSceneName - ? this.currentSceneLoadingProgress - : this.areSceneAssetsLoaded(sceneName) - ? 1 - : 0; + return this.sceneResourceLoadingQueue.getLoadingProgress(sceneName); } /** @@ -682,10 +585,7 @@ namespace gdjs { * (but maybe not parsed). */ areSceneAssetsLoaded(sceneName: string): boolean { - const sceneState = this._sceneLoadingStates.get(sceneName); - if (!sceneState) return false; - - return sceneState.status === 'loaded' || sceneState.status === 'ready'; + return this.sceneResourceLoadingQueue.areAssetsLoaded(sceneName); } /** @@ -693,14 +593,21 @@ namespace gdjs { * parsed. */ areSceneAssetsReady(sceneName: string): boolean { - const sceneState = this._sceneLoadingStates.get(sceneName); - if (!sceneState) return false; + return this.sceneResourceLoadingQueue.areAssetsReady(sceneName); + } - return sceneState.status === 'ready'; + /** + * @returns true when all the resources of the given object are loaded and + * parsed. + */ + areObjectAssetsReady(sceneName: string, objectName: string): boolean { + return this.getObjectResourceLoadingQueue(sceneName).areAssetsReady( + objectName + ); } getResource(resourceName: string): ResourceData | null { - return this._resources.get(resourceName) || null; + return this.privateResourceManager._resources.get(resourceName) || null; } // Helper methods used when resources are loaded from an URL. @@ -849,60 +756,109 @@ namespace gdjs { } /** - * Get the map of resources that are only used in the scene that is being unloaded, + * Get the resources that are only used in the scene that is being unloaded, * and that are not used in any other loaded scene (or the scene that is coming next). */ - private _getResourcesByKindOnlyUsedInUnloadedScene({ - unloadedSceneName, - newSceneName, - }: { - unloadedSceneName: string; - newSceneName: string | null; - }): Map { - const unloadedSceneState = - this._sceneLoadingStates.get(unloadedSceneName); - if (!unloadedSceneState) { - return new Map(); - } - + private _getResourcesOnlyUsedInUnloadedScene( + unloadedSceneName: string, + unloadedSceneState: LoadingTaskState, + newSceneName: string | null, + newSceneState: LoadingTaskState | null + ): Set { // Construct the set of all resources to unload. These are the resources // used in the scene that is being unloaded minus all the resources used // by the other scenes that are loaded (and the possible scene that is coming next). const resourceNamesToUnload = new Set( unloadedSceneState.resourceNames ); - for (const [ - sceneName, - sceneState, - ] of this._sceneLoadingStates.entries()) { - if (sceneName === unloadedSceneName) continue; - - if ( - sceneName === newSceneName || - sceneState.status === 'loaded' || - sceneState.status === 'ready' - ) { - sceneState.resourceNames.forEach((resourceName) => { - resourceNamesToUnload.delete(resourceName); - }); + // Also add the resources manually loaded for objects during the current scene. + // TODO Abort loading task to avoid to leave resources from an object that is currently loading. + const unloadedSceneObjectResourceLoadingQueue = newSceneName + ? this.getObjectResourceLoadingQueue(newSceneName) + : null; + if (unloadedSceneObjectResourceLoadingQueue) { + for (const objectLoadingState of unloadedSceneObjectResourceLoadingQueue.loadingStates.values()) { + for (const resourceName of objectLoadingState.resourceNames) { + resourceNamesToUnload.add(resourceName); + } } } + for (const sceneLoadingState of this.sceneResourceLoadingQueue.loadingStates.values()) { + if (sceneLoadingState === unloadedSceneState) continue; - const result = new Map(); - resourceNamesToUnload.forEach((resourceName) => { - const resourceData = this._resources.get(resourceName); - if (!resourceData) return; - - const kind = resourceData.kind; - const resources = result.get(kind); - if (resources) { - resources.push(resourceData); - } else { - result.set(kind, [resourceData]); + if ( + // We don't unload resources when the next scene need them too. + sceneLoadingState === newSceneState || + // We don't unload resources used by any loaded scene. + sceneLoadingState.status === 'loaded' || + sceneLoadingState.status === 'ready' + ) { + for (const resourceName of sceneLoadingState.resourceNames) { + resourceNamesToUnload.delete(resourceName); + } } - }); + } + // Other scenes from the stack may have loaded objects which use the same resources. + for (const objectResourceLoadingQueue of this.objectResourceLoadingQueues.values()) { + if ( + objectResourceLoadingQueue !== unloadedSceneObjectResourceLoadingQueue + ) { + for (const objectLoadingState of objectResourceLoadingQueue.loadingStates.values()) { + for (const resourceName of objectLoadingState.resourceNames) { + resourceNamesToUnload.delete(resourceName); + } + } + } + } + return resourceNamesToUnload; + } - return result; + private _getResourcesOnlyUsedInObject( + currentSceneName: string, + objectName: string, + objectLoadingState: LoadingTaskState + ): Set { + const resourceNamesToUnload = new Set( + objectLoadingState.resourceNames + ); + const currentSceneObjectResourceLoadingQueue = + this.getObjectResourceLoadingQueue(currentSceneName); + // The resources used by the current scene are already excluded from the + // object resources list at export. + // Other manually loaded objects may use the same resources. + for (const otherObjectLoadingState of currentSceneObjectResourceLoadingQueue.loadingStates.values()) { + if (otherObjectLoadingState === objectLoadingState) { + continue; + } + for (const resourceName of otherObjectLoadingState.resourceNames) { + resourceNamesToUnload.delete(resourceName); + } + } + // Other scenes may use the same resources. + for (const sceneLoadingState of this.sceneResourceLoadingQueue.loadingStates.values()) { + if ( + // We don't unload resources used by any loaded scene. + sceneLoadingState.status === 'loaded' || + sceneLoadingState.status === 'ready' + ) { + for (const resourceName of sceneLoadingState.resourceNames) { + resourceNamesToUnload.delete(resourceName); + } + } + } + // Other scenes from the stack may have loaded objects which use the same resources. + for (const objectResourceLoadingQueue of this.objectResourceLoadingQueues.values()) { + if ( + objectResourceLoadingQueue !== currentSceneObjectResourceLoadingQueue + ) { + for (const objectLoadingState of objectResourceLoadingQueue.loadingStates.values()) { + for (const resourceName of objectLoadingState.resourceNames) { + resourceNamesToUnload.delete(resourceName); + } + } + } + } + return resourceNamesToUnload; } static processWithPromisePool( @@ -973,4 +929,436 @@ namespace gdjs { return output; } } + + /** + * Give `ResourceLoadingQueue` access to private members of `ResourceManager` + * without exposing them outside. + */ + class PrivateResourceManager { + private resourceLoader: ResourceLoader; + + /** + * All the resource of a game by resource name. + */ + _resources = new Map(); + + constructor(resourceLoader: ResourceLoader) { + this.resourceLoader = resourceLoader; + } + + async _processResource(resource: ResourceData): Promise { + const resourceManager = this.resourceLoader._resourceManagersMap.get( + resource.kind + ); + if (!resourceManager) { + logger.warn( + 'Unknown resource kind: "' + + resource.kind + + '" for: "' + + resource.name + + '".' + ); + return; + } + await resourceManager.processResource(resource.name); + } + + async _loadResource(resource: ResourceData): Promise { + const resourceManager = this.resourceLoader._resourceManagersMap.get( + resource.kind + ); + if (!resourceManager) { + logger.warn( + 'Unknown resource kind: "' + + resource.kind + + '" for: "' + + resource.name + + '".' + ); + return; + } + await resourceManager.loadResource(resource.name); + } + + _unloadResource(resourceName: string): void { + const resourceData = this._resources.get(resourceName); + if (resourceData) { + const resourceManager = this.resourceLoader._resourceManagersMap.get( + resourceData.kind + ); + if (resourceManager) { + debugLogger.log( + `Unloading of resources of kind ${resourceData.kind} : ${resourceName}` + ); + resourceManager.unloadResource(resourceData); + } + } + } + } + + type ResourceDifferenceOperation = ( + unloadedTaskIdentifier: string, + unloadedTaskState: LoadingTaskState, + newTaskIdentifier: string | null, + newTaskState: LoadingTaskState | null + ) => Set; + + class ResourceLoadingQueue { + private resourceLoader: PrivateResourceManager; + private name: string; + /** + * The name of the task for which resources are currently being loaded. + */ + private currentLoadingTaskIdentifier: string = ''; + + /** + * The progress, between 0 and 1, of the loading of the resource, for the + * task that is being loaded (see `currentLoadingTaskIdentifier`). + */ + private currentTaskProgress: float = 0; + + /** + * Resources and the loading state of each task, indexed by task identifier. + * + * Don't change its state outside of `ResourceLoadingQueue`. + */ + loadingStates = new Map(); + + /** + * A queue of loading task whose resources are still to be pre-loaded. + */ + private loadingTaskQueue: Array = new Array(); + + /** + * It's set to `true` during intermediary loading screen to use a greater + * concurrency as the game is paused and doesn't need bandwidth (for video + * or music streaming or online multiplayer). + */ + isLoadingInForeground = true; + + private getResourcesDifference: ResourceDifferenceOperation; + private shouldProcessResources: boolean; + + constructor( + name: string, + resourceLoader: PrivateResourceManager, + shouldProcessResources: boolean, + getResourcesDifference: ResourceDifferenceOperation + ) { + this.name = name; + this.resourceLoader = resourceLoader; + this.shouldProcessResources = shouldProcessResources; + this.getResourcesDifference = getResourcesDifference; + } + + /** + * Load each task in order. + */ + async loadAllTasksInBackground(): Promise { + if (this.currentLoadingTaskIdentifier) { + return; + } + + debugLogger.log(`Loading all ${this.name} resources, in background.`); + while (this.loadingTaskQueue.length > 0) { + debugLogger.log( + `Still resources of ${this.loadingTaskQueue.length} ${this.name}(s) to load: ${this.loadingTaskQueue.map((task) => task.identifier).join(', ')}` + ); + const task = this.loadingTaskQueue[this.loadingTaskQueue.length - 1]; + if (task === undefined) { + continue; + } + this.currentLoadingTaskIdentifier = task.identifier; + if (!this.areAssetsLoaded(task.identifier)) { + debugLogger.log( + `Loading (but not processing) resources for ${this.name} ${task.identifier}.` + ); + const loadingState = this.loadingStates.get(task.identifier); + if (loadingState) { + await this._doLoadResources(loadingState, async (count, total) => + task.onProgress(count, total) + ); + } else { + logger.warn( + `Can\'t load resource for unknown ${this.name}: "${task.identifier}".` + ); + return; + } + debugLogger.log( + `Done loading (but not processing) resources for ${this.name} ${task.identifier}.` + ); + + // A task may have been moved last while awaiting resources to be + // downloaded (see _prioritize). + this.loadingTaskQueue.splice( + this.loadingTaskQueue.findIndex((element) => element === task), + 1 + ); + task.onFinish(); + } else { + this.loadingTaskQueue.pop(); + } + } + debugLogger.log(`${this.name} resources loading finished.`); + this.currentLoadingTaskIdentifier = ''; + } + + private async _doLoadResources( + loadingState: LoadingTaskState, + onProgress?: (count: number, total: number) => Promise + ): Promise { + let loadedCount = 0; + await ResourceLoader.processAndRetryIfNeededWithPromisePool( + loadingState.resourceNames, + this.isLoadingInForeground + ? ResourceLoader.maxForegroundConcurrency + : ResourceLoader.maxBackgroundConcurrency, + ResourceLoader.maxAttempt, + async (resourceName) => { + const resource = this.resourceLoader._resources.get(resourceName); + if (!resource) { + logger.warn('Unable to find resource "' + resourceName + '".'); + return; + } + await this.resourceLoader._loadResource(resource); + if (this.shouldProcessResources) { + await this.resourceLoader._processResource(resource); + } + loadedCount++; + this.currentTaskProgress = + loadedCount / loadingState.resourceNames.length; + onProgress && + (await onProgress(loadedCount, loadingState.resourceNames.length)); + } + ); + loadingState.status = this.shouldProcessResources ? 'ready' : 'loaded'; + } + + /** + * Process resources that are needed right away. + */ + async processResources( + taskIdentifier: string, + onProgress?: (count: number, total: number) => Promise + ): Promise { + const loadingState = this.loadingStates.get(taskIdentifier); + if (!loadingState) { + logger.warn( + `Can\'t load resource for unknown ${this.name}: "${taskIdentifier}".` + ); + return; + } + if (loadingState.status !== 'loaded') { + logger.warn( + `Resources are not loaded can\'t process them: "${taskIdentifier}".` + ); + return; + } + + let parsedCount = 0; + for (const resourceName of loadingState.resourceNames) { + const resource = this.resourceLoader._resources.get(resourceName); + if (!resource) { + logger.warn('Unable to find resource "' + resourceName + '".'); + continue; + } + await this.resourceLoader._processResource(resource); + parsedCount++; + onProgress && + (await onProgress(parsedCount, loadingState.resourceNames.length)); + } + loadingState.status = 'ready'; + } + + /** + * Put a given scene at the end of the queue. + * + * When the scene that is currently loading in background is done, + * this scene will be the next to be loaded. + */ + prioritize(taskIdentifier: string): LoadingTask | null { + const loadingState = this.loadingStates.get(taskIdentifier); + if (!loadingState) return null; + if (loadingState.status === 'loaded' || loadingState.status === 'ready') { + debugLogger.log( + `Scene ${taskIdentifier} is already loaded. Skipping prioritization.` + ); + + // The scene is already loaded, nothing to do. + return null; + } + + // The scene is not loaded: either prioritize it or add it to the loading queue. + const taskIndex = this.loadingTaskQueue.findIndex( + (task) => task.identifier === taskIdentifier + ); + let task: LoadingTask; + if (taskIndex !== -1) { + // There is already a task for this scene in the queue. + // Move it so that it's loaded first. + task = this.loadingTaskQueue[taskIndex]; + this.loadingTaskQueue.splice(taskIndex, 1); + this.loadingTaskQueue.push(task); + } else { + // There is no task for this scene in the queue. + // It might be because the scene was unloaded or never loaded. + // In this case, we need to add a new task to the queue. + task = new LoadingTask(taskIdentifier); + this.loadingTaskQueue.push(task); + } + + // Re-start the loading process in the background. While at the beginning of the game + // it's not needed because already launched, a scene might be unloaded. This means + // that we then need to relaunch the loading process. + this.loadAllTasksInBackground(); + + return task; + } + + registerResources( + taskIdentifier: string, + usedResources: Array + ) { + const objectLoadingState = this.loadingStates.get(taskIdentifier); + if (objectLoadingState) { + debugLogger.log( + `${this.name} ${taskIdentifier} is already registered.` + ); + return; + } + this.loadingStates.set(taskIdentifier, { + resourceNames: usedResources.map((resource) => resource.name), + status: 'not-loaded', + }); + } + + unregisterResources(taskIdentifier: string) { + this.loadingStates.delete(taskIdentifier); + } + + /** + * Add a task without starting it. + */ + enqueue(taskIdentifier: string): LoadingTask | null { + const objectLoadingState = this.loadingStates.get(taskIdentifier); + if (!objectLoadingState) { + debugLogger.log( + `Resources for ${this.name} ${taskIdentifier} are not registered.` + ); + return null; + } + if (objectLoadingState.status !== 'not-loaded') { + debugLogger.log( + `Resources for ${this.name} ${taskIdentifier} are already loading or loaded.` + ); + return null; + } + debugLogger.log( + `Loading of resources for ${this.name} ${taskIdentifier} was requested.` + ); + const task = new LoadingTask(taskIdentifier); + this.loadingTaskQueue.push(task); + return task; + } + + unloadResources( + unloadedTaskIdentifier: string, + newTaskIdentifier: string | null = null + ): void { + if (unloadedTaskIdentifier === newTaskIdentifier) { + return; + } + if (!unloadedTaskIdentifier) return; + debugLogger.log( + `Unloading of resources for ${this.name} ${unloadedTaskIdentifier} was requested.` + ); + + const unloadedTaskState = this.loadingStates.get(unloadedTaskIdentifier); + const newTaskState = newTaskIdentifier + ? this.loadingStates.get(newTaskIdentifier) || null + : null; + if (!unloadedTaskState) { + return; + } + for (const resourceName of this.getResourcesDifference( + unloadedTaskIdentifier, + unloadedTaskState, + newTaskIdentifier, + newTaskState + )) { + this.resourceLoader._unloadResource(resourceName); + } + + debugLogger.log( + `Unloading of resources for ${this.name} ${unloadedTaskIdentifier} finished.` + ); + + unloadedTaskState.status = 'not-loaded'; + // TODO: mark the scene as unloaded so it's not automatically loaded again eagerly. + } + + /** + * @returns true when all the resources of the given task are loaded + * (but maybe not parsed). + */ + areAssetsLoaded(taskIdentifier: string): boolean { + const loadingState = this.loadingStates.get(taskIdentifier); + if (!loadingState) return false; + + return ( + loadingState.status === 'loaded' || loadingState.status === 'ready' + ); + } + + /** + * @returns true when all the resources of the given task are loaded and + * parsed. + */ + areAssetsReady(taskIdentifier: string): boolean { + const loadingState = this.loadingStates.get(taskIdentifier); + if (!loadingState) return false; + + return loadingState.status === 'ready'; + } + + setAllResourcesAs(status: 'not-loaded' | 'loaded' | 'ready'): void { + for (const loadingState of this.loadingStates.values()) { + loadingState.status = status; + } + } + + setResourcesAs( + taskIdentifier: string, + status: 'not-loaded' | 'loaded' | 'ready' + ): void { + const loadingState = this.loadingStates.get(taskIdentifier); + if (!loadingState) { + return; + } + loadingState.status = status; + } + + getResourceNamesFor(taskIdentifier: string): Array | null { + const loadingState = this.loadingStates.get(taskIdentifier); + if (!loadingState) { + return null; + } + return loadingState.resourceNames; + } + + getLoadingProgress(taskIdentifier: string): float { + return taskIdentifier === this.currentLoadingTaskIdentifier + ? this.currentTaskProgress + : this.areAssetsLoaded(taskIdentifier) + ? 1 + : 0; + } + + clear() { + this.loadingStates.clear(); + // TODO Clearing the queue doesn't abort the running task, but it should + // not matter as resource loading is really fast in preview mode. + this.loadingTaskQueue.length = 0; + } + } } diff --git a/GDJS/Runtime/events-tools/runtimescenetools.ts b/GDJS/Runtime/events-tools/runtimescenetools.ts index ed1f6d800..6380d2468 100644 --- a/GDJS/Runtime/events-tools/runtimescenetools.ts +++ b/GDJS/Runtime/events-tools/runtimescenetools.ts @@ -363,6 +363,38 @@ namespace gdjs { ): boolean => { return runtimeScene.getGame().areSceneAssetsLoaded(sceneName); }; + + /** + * Preload an object assets in background. + */ + export const loadObjectOrGroupAssets = ( + runtimeScene: gdjs.RuntimeScene, + objectOrGroupName: string + ): void => { + runtimeScene.getGame().loadObjectOrGroupAssets(objectOrGroupName); + }; + + /** + * Unload an object assets. + */ + export const unloadObjectOrGroupAssets = ( + runtimeScene: gdjs.RuntimeScene, + objectOrGroupName: string + ): void => { + runtimeScene.getGame().unloadObjectOrGroupAssets(objectOrGroupName); + }; + + /** + * Check if object assets have finished to load in background. + */ + export const areObjectOrGroupAssetsLoaded = ( + runtimeScene: gdjs.RuntimeScene, + objectOrGroupName: string + ): boolean => { + return runtimeScene + .getGame() + .areObjectOrGroupAssetsLoaded(objectOrGroupName); + }; } } } diff --git a/GDJS/Runtime/runtimegame.ts b/GDJS/Runtime/runtimegame.ts index 9ac3e14f4..169137fcd 100644 --- a/GDJS/Runtime/runtimegame.ts +++ b/GDJS/Runtime/runtimegame.ts @@ -914,6 +914,116 @@ namespace gdjs { return this._hasJustResumed; } + /** + * Preload an object assets in background. + */ + loadObjectOrGroupAssets(objectOrGroupName: string): void { + const currentScene = this._sceneStack.getCurrentScene(); + if (!currentScene) { + return; + } + const objectGroupData = this.getObjectGroupData( + currentScene.getName(), + objectOrGroupName + ); + if (objectGroupData) { + for (const object of objectGroupData.objects) { + this._loadObjectAssets(currentScene, object.name); + } + } else { + this._loadObjectAssets(currentScene, objectOrGroupName); + } + } + + private _loadObjectAssets(currentScene: RuntimeScene, objectName: string) { + const objectData = currentScene._objects.get(objectName); + if (!objectData) { + return; + } + const usedResources = objectData.usedResources; + if (!usedResources) { + return; + } + this._resourcesLoader.loadObjectResources( + currentScene.getName(), + objectName, + usedResources + ); + } + + /** + * @returns true when all the resources of the given object are loaded. + */ + areObjectOrGroupAssetsLoaded(objectOrGroupName: string): boolean { + const currentScene = this._sceneStack.getCurrentScene(); + if (!currentScene) { + return false; + } + const objectGroupData = this.getObjectGroupData( + currentScene.getName(), + objectOrGroupName + ); + if (objectGroupData) { + for (const object of objectGroupData.objects) { + if ( + !this._resourcesLoader.areObjectAssetsReady( + currentScene.getName(), + object.name + ) + ) { + return false; + } + } + return true; + } + return this._resourcesLoader.areObjectAssetsReady( + currentScene.getName(), + objectOrGroupName + ); + } + + /** + * Unload an object assets. + */ + unloadObjectOrGroupAssets(objectOrGroupName: string): void { + const currentScene = this._sceneStack.getCurrentScene(); + if (!currentScene) { + return; + } + const objectGroupData = this.getObjectGroupData( + currentScene.getName(), + objectOrGroupName + ); + if (objectGroupData) { + for (const object of objectGroupData.objects) { + this._resourcesLoader.unloadObjectResources( + currentScene.getName(), + object.name + ); + } + } else { + this._resourcesLoader.unloadObjectResources( + currentScene.getName(), + objectOrGroupName + ); + } + } + + private getObjectGroupData( + sceneName: string, + objectGroupName: string + ): ObjectGroupData | null { + const sceneData = this.getSceneData(sceneName); + if (sceneData) { + for (const objectGroup of sceneData.objectsGroups) { + if (objectGroup.name === objectGroupName) { + return objectGroup; + } + } + } + return null; + } + /** * Preload a scene assets as soon as possible in background. */ diff --git a/GDJS/Runtime/types/project-data.d.ts b/GDJS/Runtime/types/project-data.d.ts index c807c70d2..e8d4a6a8c 100644 --- a/GDJS/Runtime/types/project-data.d.ts +++ b/GDJS/Runtime/types/project-data.d.ts @@ -38,6 +38,17 @@ declare type ObjectData = { behaviors: Array; /** The list of effects. */ effects: Array; + /** + * The resources used by this object excluding the one already used by the + * scene and/or the project. + * This attribute is only set for objects that are preloaded manually. + */ + usedResources?: ResourceReference[]; +}; + +declare type ObjectGroupData = { + name: string; + objects: Array<{ name: string }>; }; declare type GetNetworkSyncDataOptions = { @@ -277,6 +288,7 @@ declare interface InstanceContainerData { variables: RootVariableData[]; instances: InstanceData[]; objects: ObjectData[]; + objectsGroups: ObjectGroupData[]; layers: LayerData[]; } diff --git a/GDJS/tests/tests-utils/init.pixiruntimegamewithassets.js b/GDJS/tests/tests-utils/init.pixiruntimegamewithassets.js index ce329e7cd..33bb204fe 100644 --- a/GDJS/tests/tests-utils/init.pixiruntimegamewithassets.js +++ b/GDJS/tests/tests-utils/init.pixiruntimegamewithassets.js @@ -91,6 +91,7 @@ gdjs.getPixiRuntimeGameWithAssets = (props = null) => { mangledName: '', name: '', objects: [], + objectsGroups: [], layers: [], instances: [], behaviorsSharedData: [], diff --git a/GDJS/tests/tests/ResourceLoader.js b/GDJS/tests/tests/ResourceLoader.js index 5dd686746..f1d99d5f7 100644 --- a/GDJS/tests/tests/ResourceLoader.js +++ b/GDJS/tests/tests/ResourceLoader.js @@ -6,15 +6,33 @@ describe('gdjs.ResourceLoader', () => { const delay = (ms) => new Promise((res) => setTimeout(res, ms)); - /** @returns {LayoutData} */ - const createSceneData = (name, usedResources) => { + /** @typedef {{name: string, usedResources: Array}} ResourcesContainer */ + /** + * @param {ResourcesContainer & {objects?: Array}} props + * @returns {LayoutData} + */ + const createSceneData = ({ name, usedResources, objects }) => { return { r: 0, v: 0, b: 0, mangledName: name, name, - objects: [], + objects: objects + ? objects.map((object) => ({ + ...object, + type: '', + variables: [], + behaviors: [], + effects: [], + })) + : [], + objectsGroups: [ + { + name: 'MyGroup', + objects: [{ name: 'Object1' }, { name: 'Object2' }], + }, + ], layers: [], instances: [], behaviorsSharedData: [], @@ -34,25 +52,50 @@ describe('gdjs.ResourceLoader', () => { gridColor: 0, gridAlpha: 1, snap: false, - } + }, }; }; /** @type {{layouts?: LayoutData[], resources?: ResourcesData}} */ const gameSettingsWithThreeScenes = { layouts: [ - createSceneData('Scene1', [ - { name: 'scene1-resource1.png' }, - { name: 'scene1-resource2.png' }, - ]), - createSceneData('Scene2', [ - { name: 'scene2-resource1.png' }, - { name: 'shared-resource.png' }, - ]), - createSceneData('Scene3', [ - { name: 'scene3-resource1.png' }, - { name: 'shared-resource.png' }, - ]), + createSceneData({ + name: 'Scene1', + usedResources: [ + { name: 'scene1-resource1.png' }, + { name: 'scene1-resource2.png' }, + ], + objects: [ + { + name: 'Object1', + usedResources: [ + { name: 'scene1-object1-resource1.png' }, + { name: 'scene1-object1-resource2.png' }, + ], + }, + { + name: 'Object2', + usedResources: [ + { name: 'scene1-object2-resource1.png' }, + { name: 'scene1-object2-resource2.png' }, + ], + }, + ], + }), + createSceneData({ + name: 'Scene2', + usedResources: [ + { name: 'scene2-resource1.png' }, + { name: 'shared-resource.png' }, + ], + }), + createSceneData({ + name: 'Scene3', + usedResources: [ + { name: 'scene3-resource1.png' }, + { name: 'shared-resource.png' }, + ], + }), ], resources: { resources: [ @@ -70,6 +113,34 @@ describe('gdjs.ResourceLoader', () => { file: 'scene1-resource2.png', userAdded: true, }, + { + kind: 'fake-resource-kind-for-testing-only', + name: 'scene1-object1-resource1.png', + metadata: '', + file: 'scene1-object1-resource1.png', + userAdded: true, + }, + { + kind: 'fake-resource-kind-for-testing-only', + name: 'scene1-object1-resource2.png', + metadata: '', + file: 'scene1-object1-resource2.png', + userAdded: true, + }, + { + kind: 'fake-resource-kind-for-testing-only', + name: 'scene1-object2-resource1.png', + metadata: '', + file: 'scene1-object2-resource1.png', + userAdded: true, + }, + { + kind: 'fake-resource-kind-for-testing-only', + name: 'scene1-object2-resource2.png', + metadata: '', + file: 'scene1-object2-resource2.png', + userAdded: true, + }, { kind: 'fake-resource-kind-for-testing-only', name: 'scene2-resource1.png', @@ -357,4 +428,144 @@ describe('gdjs.ResourceLoader', () => { // Progress should be complete (1.0) expect(resourceLoader.getSceneLoadingProgress('Scene2')).to.be(1); }); + + /** + * @param {gdjs.RuntimeGame} runtimeGame + * @param {gdjs.MockedResourceManager} mockedResourceManager + */ + const loadObject1AndCheck = async (runtimeGame, mockedResourceManager) => { + runtimeGame.loadObjectOrGroupAssets('Object1'); + + // Object1 resources should be pending download + expect( + mockedResourceManager.isResourceDownloadPending( + 'scene1-object1-resource1.png' + ) + ).to.be(true); + expect( + mockedResourceManager.isResourceDownloadPending( + 'scene1-object1-resource2.png' + ) + ).to.be(true); + expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object1')).to.be(false); + + // Mark Object1 resources as loaded + mockedResourceManager.markPendingResourcesAsLoaded( + 'scene1-object1-resource1.png' + ); + mockedResourceManager.markPendingResourcesAsLoaded( + 'scene1-object1-resource2.png' + ); + await delay(10); + + // Object1 should now be ready + expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object1')).to.be(true); + }; + + it('can load object resources with an action', async () => { + const mockedResourceManager = new gdjs.MockedResourceManager(); + const runtimeGame = gdjs.getPixiRuntimeGame(gameSettingsWithThreeScenes); + const resourceLoader = runtimeGame.getResourceLoader(); + resourceLoader.injectMockResourceManagerForTesting( + 'fake-resource-kind-for-testing-only', + mockedResourceManager + ); + + // Start loading first scene and background loading + runtimeGame.loadFirstAssetsAndStartBackgroundLoading('Scene1'); + // Mark Scene1 resources as loaded + mockedResourceManager.markPendingResourcesAsLoaded('scene1-resource1.png'); + mockedResourceManager.markPendingResourcesAsLoaded('scene1-resource2.png'); + await delay(10); + + // Scene1 should now be ready + expect(runtimeGame.areSceneAssetsLoaded('Scene1')).to.be(true); + expect(runtimeGame.areSceneAssetsReady('Scene1')).to.be(true); + + runtimeGame._sceneStack.push('Scene1'); + await loadObject1AndCheck(runtimeGame, mockedResourceManager); + + runtimeGame.unloadObjectOrGroupAssets('Object1'); + expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object1')).to.be(false); + + await loadObject1AndCheck(runtimeGame, mockedResourceManager); + }); + + it('can load object group resources with an action', async () => { + const mockedResourceManager = new gdjs.MockedResourceManager(); + const runtimeGame = gdjs.getPixiRuntimeGame(gameSettingsWithThreeScenes); + const resourceLoader = runtimeGame.getResourceLoader(); + resourceLoader.injectMockResourceManagerForTesting( + 'fake-resource-kind-for-testing-only', + mockedResourceManager + ); + + // Start loading first scene and background loading + runtimeGame.loadFirstAssetsAndStartBackgroundLoading('Scene1'); + // Mark Scene1 resources as loaded + mockedResourceManager.markPendingResourcesAsLoaded('scene1-resource1.png'); + mockedResourceManager.markPendingResourcesAsLoaded('scene1-resource2.png'); + await delay(10); + + // Scene1 should now be ready + expect(runtimeGame.areSceneAssetsLoaded('Scene1')).to.be(true); + expect(runtimeGame.areSceneAssetsReady('Scene1')).to.be(true); + + runtimeGame._sceneStack.push('Scene1'); + runtimeGame.loadObjectOrGroupAssets('MyGroup'); + + // Object1 resources should be pending download + expect( + mockedResourceManager.isResourceDownloadPending( + 'scene1-object1-resource1.png' + ) + ).to.be(true); + expect( + mockedResourceManager.isResourceDownloadPending( + 'scene1-object1-resource2.png' + ) + ).to.be(true); + expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object1')).to.be(false); + expect(runtimeGame.areObjectOrGroupAssetsLoaded('MyGroup')).to.be(false); + + // Mark Object1 resources as loaded + mockedResourceManager.markPendingResourcesAsLoaded( + 'scene1-object1-resource1.png' + ); + mockedResourceManager.markPendingResourcesAsLoaded( + 'scene1-object1-resource2.png' + ); + await delay(10); + + // Object1 should now be ready + expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object1')).to.be(true); + expect(runtimeGame.areObjectOrGroupAssetsLoaded('MyGroup')).to.be(false); + + // Object2 resources should be pending download + expect( + mockedResourceManager.isResourceDownloadPending( + 'scene1-object2-resource1.png' + ) + ).to.be(true); + expect( + mockedResourceManager.isResourceDownloadPending( + 'scene1-object2-resource2.png' + ) + ).to.be(true); + expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object2')).to.be(false); + expect(runtimeGame.areObjectOrGroupAssetsLoaded('MyGroup')).to.be(false); + + mockedResourceManager.markPendingResourcesAsLoaded( + 'scene1-object2-resource1.png' + ); + mockedResourceManager.markPendingResourcesAsLoaded( + 'scene1-object2-resource2.png' + ); + await delay(10); + + // Object1 and Object2 should now be ready + expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object1')).to.be(true); + expect(runtimeGame.areObjectOrGroupAssetsLoaded('Object2')).to.be(true); + expect(runtimeGame.areObjectOrGroupAssetsLoaded('MyGroup')).to.be(true); + }); }); diff --git a/GDJS/tests/tests/effects.js b/GDJS/tests/tests/effects.js index 69f759536..d300246bd 100644 --- a/GDJS/tests/tests/effects.js +++ b/GDJS/tests/tests/effects.js @@ -105,6 +105,7 @@ describe('gdjs.EffectsManager', () => { title: '', behaviorsSharedData: [], objects: [], + objectsGroups: [], instances: [], usedResources: [], uiSettings: { diff --git a/GDJS/tests/tests/hot-reloader.js b/GDJS/tests/tests/hot-reloader.js index a2402f76f..5d256991d 100644 --- a/GDJS/tests/tests/hot-reloader.js +++ b/GDJS/tests/tests/hot-reloader.js @@ -182,6 +182,7 @@ describe('gdjs.HotReloader._hotReloadRuntimeGame', () => { objects: objects || [ createSpriteData({ name: 'MyObject', image: 'ResourceA' }), ], + objectsGroups: [], instances: instances ? instances.map((instance) => ({ ...defaultInstance, ...instance })) : [], @@ -221,6 +222,7 @@ describe('gdjs.HotReloader._hotReloadRuntimeGame', () => { objects: objects || [ createSpriteData({ name: 'MyChildObject', image: 'ResourceA' }), ], + objectsGroups: [], layers: [baseLayer], areaMinX: 0, areaMinY: 0, diff --git a/GDJS/tests/tests/runtimescene.js b/GDJS/tests/tests/runtimescene.js index ac2896cbe..e3b773b2d 100644 --- a/GDJS/tests/tests/runtimescene.js +++ b/GDJS/tests/tests/runtimescene.js @@ -45,6 +45,7 @@ describe('gdjs.RuntimeScene integration tests', function () { effects: [], }, ], + objectsGroups: [], instances: [], usedResources: [], uiSettings: { diff --git a/GDJS/tests/tests/scenestack.js b/GDJS/tests/tests/scenestack.js index d31880fa9..9ddd78ad3 100644 --- a/GDJS/tests/tests/scenestack.js +++ b/GDJS/tests/tests/scenestack.js @@ -14,6 +14,7 @@ describe('gdjs.SceneStack', () => { mangledName: name, name, objects: [], + objectsGroups: [], layers: [], instances: [], behaviorsSharedData: [], diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl index 319db7553..fb0200af0 100644 --- a/GDevelop.js/Bindings/Bindings.idl +++ b/GDevelop.js/Bindings/Bindings.idl @@ -914,6 +914,8 @@ interface gdObject { [Const, Ref] DOMString GetAssetStoreId(); void SetType([Const] DOMString type); [Const, Ref] DOMString GetType(); + void SetResourcesPreloading([Const] DOMString value); + [Const, Ref] DOMString GetResourcesPreloading(); [Ref] ObjectConfiguration GetConfiguration(); diff --git a/GDevelop.js/types.d.ts b/GDevelop.js/types.d.ts index b941c9f28..507283512 100644 --- a/GDevelop.js/types.d.ts +++ b/GDevelop.js/types.d.ts @@ -791,6 +791,8 @@ export class gdObject extends EmscriptenObject { getAssetStoreId(): string; setType(type: string): void; getType(): string; + setResourcesPreloading(value: string): void; + getResourcesPreloading(): string; getConfiguration(): ObjectConfiguration; getVariables(): VariablesContainer; getEffects(): EffectsContainer; diff --git a/GDevelop.js/types/gdobject.js b/GDevelop.js/types/gdobject.js index fb2b809ff..80cdc698f 100644 --- a/GDevelop.js/types/gdobject.js +++ b/GDevelop.js/types/gdobject.js @@ -8,6 +8,8 @@ declare class gdObject { getAssetStoreId(): string; setType(type: string): void; getType(): string; + setResourcesPreloading(value: string): void; + getResourcesPreloading(): string; getConfiguration(): gdObjectConfiguration; getVariables(): gdVariablesContainer; getEffects(): gdEffectsContainer; diff --git a/newIDE/app/src/CompactPropertiesEditor/CompactPropertiesEditorByVisibility.js b/newIDE/app/src/CompactPropertiesEditor/CompactPropertiesEditorByVisibility.js index 29fc1c940..5d3bfbbc7 100644 --- a/newIDE/app/src/CompactPropertiesEditor/CompactPropertiesEditorByVisibility.js +++ b/newIDE/app/src/CompactPropertiesEditor/CompactPropertiesEditorByVisibility.js @@ -156,7 +156,7 @@ export const CompactCollapsibleAdvancedSection = ({ uncollapsedByDefault ); return showAdvancedOptions ? ( - + {children} - + ) : ( ); +const resourcesPreloadingFieldChoices: Array = [ + { + value: 'with-scene', + label: t`Preload with the scene`, + }, + { + value: 'manually', + label: t`Preload with an action`, + }, +]; +const getResourcesPreloadingField = ({ i18n }: {| i18n: I18nType |}): Field => { + return { + name: 'LoadingSettings', + title: i18n._(t`Loading`), + type: 'column', + children: [ + { + name: 'ResourcesPreloading', + getLabel: () => i18n._(t`Resources preloading`), + valueType: 'string', + getChoices: () => resourcesPreloadingFieldChoices, + getValue: ({ object }: { object: gdObject }): string => + object.getResourcesPreloading(), + setValue: ({ object }: { object: gdObject }, newValue: string) => + object.setResourcesPreloading(newValue), + defaultValue: 'with-scene', + visibility: 'advanced', + }, + ], + }; +}; + type Props = {| project: gdProject, resourceManagementProps: ResourceManagementProps, @@ -508,7 +544,7 @@ export const CompactObjectPropertiesEditor = ({ // schemaRecomputeTrigger allows to invalidate the schema when required. } const properties = objectConfigurationAsGd.getProperties(); - return propertiesMapToSchema({ + const schema = propertiesMapToSchema({ properties, defaultValueProperties: customObjectEventsBasedObject ? customObjectEventsBasedObject.getPropertyDescriptors() @@ -526,13 +562,20 @@ export const CompactObjectPropertiesEditor = ({ object, visibility: 'All', }); + + if (layout && layout.getObjects().hasObjectNamed(object.getName())) { + schema.push(getResourcesPreloadingField({ i18n })); + } + return schema; }, [ schemaRecomputeTrigger, objectConfigurationAsGd, - object, customObjectEventsBasedObject, + object, + layout, onObjectsModified, + i18n, ] );