diff --git a/Core/GDCore/IDE/ObjectAssetSerializer.cpp b/Core/GDCore/IDE/ObjectAssetSerializer.cpp new file mode 100644 index 000000000000..840722091dca --- /dev/null +++ b/Core/GDCore/IDE/ObjectAssetSerializer.cpp @@ -0,0 +1,110 @@ +/* + * GDevelop Core + * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#include "ObjectAssetSerializer.h" + +#include "GDCore/Extensions/Metadata/BehaviorMetadata.h" +#include "GDCore/Extensions/Metadata/MetadataProvider.h" +#include "GDCore/Extensions/Platform.h" +#include "GDCore/Extensions/PlatformExtension.h" +#include "GDCore/IDE/Project/ResourcesInUseHelper.h" +#include "GDCore/Project/Behavior.h" +#include "GDCore/Project/CustomBehavior.h" +#include "GDCore/Project/EventsFunctionsExtension.h" +#include "GDCore/Project/Layout.h" +#include "GDCore/Project/Object.h" +#include "GDCore/Project/Project.h" +#include "GDCore/Project/PropertyDescriptor.h" +#include "GDCore/Serialization/SerializerElement.h" +#include "GDCore/Tools/Log.h" + + +namespace gd { + +const std::vector ObjectAssetSerializer::resourceTypes = { + "image", "audio", "font", "json", + "tilemap", "tileset", "video", "bitmapFont"}; + +void ObjectAssetSerializer::SerializeTo(gd::Project &project, + const gd::Object &object, + SerializerElement &element) { + auto cleanObject = object.Clone(); + cleanObject->GetVariables().Clear(); + cleanObject->GetEffects().Clear(); + for (auto &&behaviorName : cleanObject->GetAllBehaviorNames()) { + cleanObject->RemoveBehavior(behaviorName); + } + + const gd::String &type = cleanObject->GetType(); + const auto separatorIndex = + type.find(PlatformExtension::GetNamespaceSeparator()); + gd::String extensionName = + separatorIndex != std::string::npos ? type.substr(0, separatorIndex) : ""; + + element.SetAttribute("id", ""); + element.SetAttribute("name", ""); + element.SetAttribute("license", ""); + if (project.HasEventsFunctionsExtensionNamed(extensionName)) { + auto &extension = project.GetEventsFunctionsExtension(extensionName); + element.SetAttribute("description", extension.GetShortDescription()); + } + element.SetAttribute("gdevelopVersion", ""); + element.SetAttribute("version", ""); + element.SetIntAttribute("animationsCount", 1); + element.SetIntAttribute("maxFramesCount", 1); + // TODO Find the right object dimensions. + element.SetIntAttribute("width", 0); + element.SetIntAttribute("height", 0); + SerializerElement &authorsElement = element.AddChild("authors"); + authorsElement.ConsiderAsArrayOf("author"); + SerializerElement &tagsElement = element.AddChild("tags"); + tagsElement.ConsiderAsArrayOf("tag"); + + SerializerElement &objectAssetsElement = element.AddChild("objectAssets"); + objectAssetsElement.ConsiderAsArrayOf("objectAsset"); + SerializerElement &objectAssetElement = + objectAssetsElement.AddChild("objectAsset"); + + cleanObject->SerializeTo(objectAssetElement.AddChild("object")); + + SerializerElement &resourcesElement = + objectAssetElement.AddChild("resources"); + resourcesElement.ConsiderAsArrayOf("resource"); + auto &resourcesManager = project.GetResourcesManager(); + gd::ResourcesInUseHelper resourcesInUse(resourcesManager); + cleanObject->GetConfiguration().ExposeResources(resourcesInUse); + for (auto &&resourceType : resourceTypes) { + for (auto &&resourceName : resourcesInUse.GetAll(resourceType)) { + if (resourceName.length() == 0) { + continue; + } + auto &resource = resourcesManager.GetResource(resourceName); + SerializerElement &resourceElement = + resourcesElement.AddChild("resource"); + resourceElement.SetAttribute("name", resourceName); + resourceElement.SetAttribute("file", resource.GetFile()); + resourceElement.SetAttribute("kind", resource.GetKind()); + resourceElement.SetBoolAttribute("alwaysLoaded", false); + resourceElement.SetAttribute("metadata", resource.GetMetadata()); + } + } + + SerializerElement &requiredExtensionsElement = + objectAssetElement.AddChild("requiredExtensions"); + requiredExtensionsElement.ConsiderAsArrayOf("requiredExtension"); + if (project.HasEventsFunctionsExtensionNamed(extensionName)) { + SerializerElement &requiredExtensionElement = + requiredExtensionsElement.AddChild("requiredExtension"); + requiredExtensionElement.SetAttribute("extensionName", extensionName); + requiredExtensionElement.SetAttribute("extensionVersion", "1.0.0"); + } + + // TODO This can be removed when the asset script no longer require it. + SerializerElement &customizationElement = + objectAssetElement.AddChild("customization"); + customizationElement.ConsiderAsArrayOf("empty"); +} + +} // namespace gd diff --git a/Core/GDCore/IDE/ObjectAssetSerializer.h b/Core/GDCore/IDE/ObjectAssetSerializer.h new file mode 100644 index 000000000000..5fe9c8c6d3a7 --- /dev/null +++ b/Core/GDCore/IDE/ObjectAssetSerializer.h @@ -0,0 +1,46 @@ +/* + * GDevelop Core + * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#pragma once +#include + +#include "GDCore/String.h" + +namespace gd { +class Object; +class ExtensionDependency; +class PropertyDescriptor; +class Project; +class Layout; +class ArbitraryResourceWorker; +class InitialInstance; +class SerializerElement; +class EffectsContainer; +} // namespace gd + +namespace gd { + +/** + * \brief Serialize objects into an asset for the store. + * + * \ingroup IDE + */ +class GD_CORE_API ObjectAssetSerializer { +public: + /** + * \brief Serialize the object into an asset. + */ + static void SerializeTo(gd::Project &project, const gd::Object &object, + SerializerElement &element); + + ~ObjectAssetSerializer(){}; + +private: + ObjectAssetSerializer(){}; + + static const std::vector resourceTypes; +}; + +} // namespace gd diff --git a/Core/GDCore/IDE/Project/AssetResourcesMergingHelper.cpp b/Core/GDCore/IDE/Project/AssetResourcesMergingHelper.cpp new file mode 100644 index 000000000000..1e6e0b25d27e --- /dev/null +++ b/Core/GDCore/IDE/Project/AssetResourcesMergingHelper.cpp @@ -0,0 +1,56 @@ +/* + * GDevelop Core + * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ + +#include "AssetResourcesMergingHelper.h" +#include "GDCore/Project/Project.h" +#include "GDCore/Project/ResourcesManager.h" +#include "GDCore/String.h" + + +namespace gd { + +void AssetResourcesMergingHelper::ExposeImage(gd::String &imageName) { + ExposeResourceAsFile(imageName); +} + +void AssetResourcesMergingHelper::ExposeAudio(gd::String &audioName) { + ExposeResourceAsFile(audioName); +} + +void AssetResourcesMergingHelper::ExposeFont(gd::String &fontName) { + ExposeResourceAsFile(fontName); +} + +void AssetResourcesMergingHelper::ExposeJson(gd::String &jsonName) { + ExposeResourceAsFile(jsonName); +} + +void AssetResourcesMergingHelper::ExposeTilemap(gd::String &tilemapName) { + ExposeResourceAsFile(tilemapName); +} + +void AssetResourcesMergingHelper::ExposeTileset(gd::String &tilesetName) { + ExposeResourceAsFile(tilesetName); +} + +void AssetResourcesMergingHelper::ExposeVideo(gd::String &videoName) { + ExposeResourceAsFile(videoName); +} + +void AssetResourcesMergingHelper::ExposeBitmapFont(gd::String &bitmapFontName) { + ExposeResourceAsFile(bitmapFontName); +} + +void AssetResourcesMergingHelper::ExposeResourceAsFile( + gd::String &resourceName) { + + auto &resource = project.GetResourcesManager().GetResource(resourceName); + gd::String file = resource.GetFile(); + ExposeFile(file); + resourceName = file; +} + +} // namespace gd diff --git a/Core/GDCore/IDE/Project/AssetResourcesMergingHelper.h b/Core/GDCore/IDE/Project/AssetResourcesMergingHelper.h new file mode 100644 index 000000000000..3c65848d9e06 --- /dev/null +++ b/Core/GDCore/IDE/Project/AssetResourcesMergingHelper.h @@ -0,0 +1,54 @@ +/* + * GDevelop Core + * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#pragma once + +#include "GDCore/IDE/Project/ArbitraryResourceWorker.h" +#include "GDCore/IDE/Project/ResourcesMergingHelper.h" +#include "GDCore/String.h" +#include +#include +#include + +namespace gd { +class AbstractFileSystem; +class Project; +} // namespace gd + +namespace gd { + +/** + * \brief ResourcesMergingHelper is used (mainly during export) + * to list resources and generate new filenames, to allow them to be all copied + * in a single directory (potentially changing the filename to avoid conflicts, + * but preserving extensions). + * + * \see ArbitraryResourceWorker + * + * \ingroup IDE + */ +class GD_CORE_API AssetResourcesMergingHelper : public ResourcesMergingHelper { +public: + AssetResourcesMergingHelper(gd::Project &project_, + gd::AbstractFileSystem &fileSystem) + : ResourcesMergingHelper(project_.GetResourcesManager(), fileSystem), project(project_){}; + virtual ~AssetResourcesMergingHelper(){}; + + void ExposeImage(gd::String &imageName) override; + void ExposeAudio(gd::String &audioName) override; + void ExposeFont(gd::String &fontName) override; + void ExposeJson(gd::String &jsonName) override; + void ExposeTilemap(gd::String &tilemapName) override; + void ExposeTileset(gd::String &tilesetName) override; + void ExposeVideo(gd::String &videoName) override; + void ExposeBitmapFont(gd::String &bitmapFontName) override; + +protected: + void ExposeResourceAsFile(gd::String &resourceName); + + gd::Project &project; +}; + +} // namespace gd diff --git a/Core/GDCore/IDE/Project/ProjectResourcesCopier.cpp b/Core/GDCore/IDE/Project/ProjectResourcesCopier.cpp index dca3d3fed8cf..4fec5574ac8d 100644 --- a/Core/GDCore/IDE/Project/ProjectResourcesCopier.cpp +++ b/Core/GDCore/IDE/Project/ProjectResourcesCopier.cpp @@ -5,13 +5,17 @@ */ #include "ProjectResourcesCopier.h" #include +#include #include "GDCore/CommonTools.h" #include "GDCore/IDE/AbstractFileSystem.h" #include "GDCore/IDE/Project/ResourcesAbsolutePathChecker.h" #include "GDCore/IDE/Project/ResourcesMergingHelper.h" +#include "GDCore/IDE/Project/AssetResourcesMergingHelper.h" #include "GDCore/Project/Project.h" +#include "GDCore/Project/Object.h" #include "GDCore/Tools/Localization.h" #include "GDCore/Tools/Log.h" +#include "GDCore/Extensions/Builtin/SpriteExtension/SpriteObject.h" #include "GDCore/IDE/ResourceExposer.h" using namespace std; @@ -65,12 +69,122 @@ bool ProjectResourcesCopier::CopyAllResourcesTo( resourcesMergingHelper); // Copy resources - map& resourcesNewFilename = + CopyResourcesTo(resourcesMergingHelper.GetAllResourcesOldAndNewFilename(), fs, destinationDirectory); + + return true; +} + +bool ProjectResourcesCopier::CopyObjectResourcesTo( + gd::Project &project, gd::Object &object, AbstractFileSystem &fs, + const gd::String &destinationDirectory, const gd::String &objectFullName) { + auto projectDirectory = fs.DirNameFrom(project.GetProjectFile()); + std::cout << "Copying some resources from " << projectDirectory << " to " + << destinationDirectory << "..." << std::endl; + + // Get the resources to be copied + gd::AssetResourcesMergingHelper resourcesMergingHelper(project, fs); + resourcesMergingHelper.SetBaseDirectory(projectDirectory); + resourcesMergingHelper.PreserveDirectoriesStructure(false); + resourcesMergingHelper.PreserveAbsoluteFilenames(false); + + object.GetConfiguration().ExposeResources(resourcesMergingHelper); + auto &resourcesNewFileNames = resourcesMergingHelper.GetAllResourcesOldAndNewFilename(); + + NormalizeResourceNames(object, resourcesNewFileNames, objectFullName); + + CopyResourcesTo(resourcesNewFileNames, fs, destinationDirectory); + + return true; +} + +void ProjectResourcesCopier::NormalizeResourceNames( + gd::Object &object, std::map &resourcesNewFileNames, + const gd::String &objectFullName) { + + if (object.GetConfiguration().GetType() == "Sprite") { + gd::SpriteObject &spriteConfiguration = + dynamic_cast(object.GetConfiguration()); + std::map normalizedFileNames; + + for (std::size_t animationIndex = 0; + animationIndex < spriteConfiguration.GetAnimationsCount(); + animationIndex++) { + auto &animation = spriteConfiguration.GetAnimation(animationIndex); + auto &direction = animation.GetDirection(0); + + const gd::String &animationName = animation.GetName().empty() + ? gd::String::From(animationIndex) + : animation.GetName(); + + // Search frames that share the same resource. + map> frameIndexes; + for (std::size_t frameIndex = 0; frameIndex < direction.GetSpritesCount(); + frameIndex++) { + auto &frame = direction.GetSprite(frameIndex); + + if (frameIndexes.find(frame.GetImageName()) == frameIndexes.end()) { + std::vector emptyVector; + frameIndexes[frame.GetImageName()] = emptyVector; + } + auto &indexes = frameIndexes[frame.GetImageName()]; + indexes.push_back(frameIndex); + } + + for (std::size_t frameIndex = 0; frameIndex < direction.GetSpritesCount(); + frameIndex++) { + auto &frame = direction.GetSprite(frameIndex); + auto oldName = frame.GetImageName(); + + if (normalizedFileNames.find(oldName) != normalizedFileNames.end()) { + gd::LogWarning("The resource \"" + oldName + + "\" is shared by several animations."); + continue; + } + + gd::String newName = objectFullName; + if (spriteConfiguration.GetAnimationsCount() > 1) { + newName += "_" + animationName; + } + if (direction.GetSpritesCount() > 1) { + newName += "_"; + auto &indexes = frameIndexes[frame.GetImageName()]; + for (size_t i = 0; i < indexes.size(); i++) { + newName += gd::String::From(indexes.at(i) + 1); + if (i < indexes.size() - 1) { + newName += ";"; + } + } + } + gd::String extension = oldName.substr(oldName.find_last_of(".")); + newName += extension; + + frame.SetImageName(newName); + normalizedFileNames[oldName] = newName; + } + } + for (map::const_iterator it = + resourcesNewFileNames.begin(); + it != resourcesNewFileNames.end(); ++it) { + if (!it->first.empty()) { + gd::String originFile = it->first; + gd::String destinationFile = it->second; + resourcesNewFileNames[originFile] = + normalizedFileNames[destinationFile]; + } + } + } +} + +void ProjectResourcesCopier::CopyResourcesTo( + map& resourcesNewFileNames, + AbstractFileSystem& fs, + const gd::String &destinationDirectory) { for (map::const_iterator it = - resourcesNewFilename.begin(); - it != resourcesNewFilename.end(); + resourcesNewFileNames.begin(); + it != resourcesNewFileNames.end(); ++it) { + std::cout << it->first << " --> " << it->second << std::endl; if (!it->first.empty()) { // Create the destination filename gd::String destinationFile = it->second; @@ -87,8 +201,6 @@ bool ProjectResourcesCopier::CopyAllResourcesTo( } } } - - return true; } } // namespace gd diff --git a/Core/GDCore/IDE/Project/ProjectResourcesCopier.h b/Core/GDCore/IDE/Project/ProjectResourcesCopier.h index 2cd5eacdb27e..72e995e4535d 100644 --- a/Core/GDCore/IDE/Project/ProjectResourcesCopier.h +++ b/Core/GDCore/IDE/Project/ProjectResourcesCopier.h @@ -3,11 +3,14 @@ * Copyright 2008-present Florian Rival (Florian.Rival@gmail.com). All rights * reserved. This project is released under the MIT License. */ -#ifndef PROJECTRESOURCESCOPIER_H -#define PROJECTRESOURCESCOPIER_H +#pragma once + #include "GDCore/String.h" +#include + namespace gd { class Project; +class Object; class AbstractFileSystem; } // namespace gd @@ -47,6 +50,24 @@ class GD_CORE_API ProjectResourcesCopier { bool updateOriginalProject, bool preserveAbsoluteFilenames = true, bool preserveDirectoryStructure = true); + + /** + * \brief Copy all resources files of an object to the specified + * `destinationDirectory` to prepare asset archive export. + * + * \param project The object project + * \param object The object to be used + * \param fs The abstract file system to be used + * \param destinationDirectory The directory where resources must be copied to + * \param objectFullName The name to use in file names of sprite resources + * + * \return true if no error happened + */ + static bool CopyObjectResourcesTo(gd::Project &project, gd::Object &object, + gd::AbstractFileSystem &fs, + const gd::String &destinationDirectory, + const gd::String &objectFullName); + private: static bool CopyAllResourcesTo(gd::Project& originalProject, gd::Project& clonedProject, @@ -54,8 +75,16 @@ class GD_CORE_API ProjectResourcesCopier { gd::String destinationDirectory, bool preserveAbsoluteFilenames = true, bool preserveDirectoryStructure = true); + + static void + CopyResourcesTo(std::map &resourcesNewFileNames, + AbstractFileSystem &fs, + const gd::String &destinationDirectory); + + static void NormalizeResourceNames( + gd::Object &object, + std::map &resourcesNewFileNames, + const gd::String &objectFullName); }; } // namespace gd - -#endif // PROJECTRESOURCESCOPIER_H diff --git a/Core/GDCore/IDE/Project/ResourcesMergingHelper.cpp b/Core/GDCore/IDE/Project/ResourcesMergingHelper.cpp index 162eee6e8098..25dd9bb18c28 100644 --- a/Core/GDCore/IDE/Project/ResourcesMergingHelper.cpp +++ b/Core/GDCore/IDE/Project/ResourcesMergingHelper.cpp @@ -28,7 +28,7 @@ void ResourcesMergingHelper::ExposeFile(gd::String& resourceFilename) { auto stripToFilenameOnly = [&]() { fs.MakeAbsolute(resourceFullFilename, baseDirectory); SetNewFilename(resourceFullFilename, fs.FileNameFrom(resourceFullFilename)); - resourceFilename = oldFilenames[resourceFullFilename]; + resourceFilename = newFilenames[resourceFullFilename]; }; // if we do not want to preserve the folders at all, @@ -45,7 +45,7 @@ void ResourcesMergingHelper::ExposeFile(gd::String& resourceFilename) { gd::String relativeFilename = resourceFullFilename; if (fs.MakeRelative(relativeFilename, baseDirectory)) { SetNewFilename(resourceFullFilename, relativeFilename); - resourceFilename = oldFilenames[resourceFullFilename]; + resourceFilename = newFilenames[resourceFullFilename]; } else { // The filename cannot be made relative. Consider that it is absolute. // Just strip the filename to its file part @@ -63,7 +63,7 @@ void ResourcesMergingHelper::ExposeFile(gd::String& resourceFilename) { void ResourcesMergingHelper::SetNewFilename(gd::String oldFilename, gd::String newFilename) { - if (oldFilenames.find(oldFilename) != oldFilenames.end()) return; + if (newFilenames.find(oldFilename) != newFilenames.end()) return; // Extract baseName and extension from the new filename size_t extensionPos = newFilename.find_last_of("."); @@ -80,13 +80,13 @@ void ResourcesMergingHelper::SetNewFilename(gd::String oldFilename, gd::NewNameGenerator::Generate( baseName, [this, extension](const gd::String& newBaseName) { - return newFilenames.find(newBaseName + extension) != - newFilenames.end(); + return oldFilenames.find(newBaseName + extension) != + oldFilenames.end(); }) + extension; - oldFilenames[oldFilename] = finalFilename; - newFilenames[finalFilename] = oldFilename; + newFilenames[oldFilename] = finalFilename; + oldFilenames[finalFilename] = oldFilename; } void ResourcesMergingHelper::SetBaseDirectory( diff --git a/Core/GDCore/IDE/Project/ResourcesMergingHelper.h b/Core/GDCore/IDE/Project/ResourcesMergingHelper.h index ef8d5bc1a759..9a9434adfdcc 100644 --- a/Core/GDCore/IDE/Project/ResourcesMergingHelper.h +++ b/Core/GDCore/IDE/Project/ResourcesMergingHelper.h @@ -64,7 +64,7 @@ class GD_CORE_API ResourcesMergingHelper : public ArbitraryResourceWorker { * the Base Directory. */ std::map& GetAllResourcesOldAndNewFilename() { - return oldFilenames; + return newFilenames; }; /** @@ -76,7 +76,13 @@ class GD_CORE_API ResourcesMergingHelper : public ArbitraryResourceWorker { protected: void SetNewFilename(gd::String oldFilename, gd::String newFilename); + /** + * Original file names that can be accessed by their new name. + */ std::map oldFilenames; + /** + * New file names that can be accessed by their original name. + */ std::map newFilenames; gd::String baseDirectory; bool preserveDirectoriesStructure; ///< If set to true, the directory diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl index 853d4e3a9c18..f0b5be4cf8b9 100644 --- a/GDevelop.js/Bindings/Bindings.idl +++ b/GDevelop.js/Bindings/Bindings.idl @@ -1297,6 +1297,10 @@ interface Serializer { [Value] SerializerElement STATIC_FromJSON([Const] DOMString json); }; +interface ObjectAssetSerializer { + void STATIC_SerializeTo([Ref] Project project, [Const, Ref] gdObject obj, [Ref] SerializerElement element); +}; + interface InstructionsList { void InstructionsList(); @@ -3015,6 +3019,11 @@ interface ProjectResourcesCopier { boolean updateOriginalProject, boolean preserveAbsoluteFilenames, boolean preserveDirectoryStructure); + boolean STATIC_CopyObjectResourcesTo([Ref] Project project, + [Ref] gdObject originalObject, + [Ref] AbstractFileSystem fs, + [Const] DOMString destinationDirectory, + [Const] DOMString objectFullName); }; interface ObjectsUsingResourceCollector { diff --git a/GDevelop.js/Bindings/Wrapper.cpp b/GDevelop.js/Bindings/Wrapper.cpp index 2ed2faed1c5e..ccfd3a8ecb5c 100644 --- a/GDevelop.js/Bindings/Wrapper.cpp +++ b/GDevelop.js/Bindings/Wrapper.cpp @@ -86,6 +86,7 @@ #include #include #include +#include #include #include #include @@ -539,6 +540,7 @@ typedef ExtensionAndMetadata ExtensionAndExpressionMetadata; #define STATIC_GetSafeName GetSafeName #define STATIC_ToJSON ToJSON #define STATIC_FromJSON(x) FromJSON(x) +#define STATIC_SerializeTo SerializeTo #define STATIC_IsObject IsObject #define STATIC_IsBehavior IsBehavior #define STATIC_IsExpression IsExpression @@ -754,6 +756,7 @@ typedef ExtensionAndMetadata ExtensionAndExpressionMetadata; #define STATIC_ShiftSentenceParamIndexes ShiftSentenceParamIndexes #define STATIC_CopyAllResourcesTo CopyAllResourcesTo +#define STATIC_CopyObjectResourcesTo CopyObjectResourcesTo #define STATIC_IsExtensionLifecycleEventsFunction \ IsExtensionLifecycleEventsFunction diff --git a/GDevelop.js/scripts/generate-types.js b/GDevelop.js/scripts/generate-types.js index 2bdb1a3fa5ee..aa3183b3cecd 100644 --- a/GDevelop.js/scripts/generate-types.js +++ b/GDevelop.js/scripts/generate-types.js @@ -307,6 +307,12 @@ type ParticleEmitterObject_RendererType = 0 | 1 | 2` 'declare class gdGroupEvent extends gdBaseEvent {', 'types/gdgroupevent.js' ); + shell.sed( + '-i', + 'declare class gdAbstractFileSystemJS {', + 'declare class gdAbstractFileSystemJS extends gdAbstractFileSystem {', + 'types/gdabstractfilesystemjs.js' + ); [ 'BaseEvent', 'StandardEvent', diff --git a/GDevelop.js/types/gdabstractfilesystemjs.js b/GDevelop.js/types/gdabstractfilesystemjs.js index e9b2d05d4974..54c154fd0a8c 100644 --- a/GDevelop.js/types/gdabstractfilesystemjs.js +++ b/GDevelop.js/types/gdabstractfilesystemjs.js @@ -1,5 +1,5 @@ // Automatically generated by GDevelop.js/scripts/generate-types.js -declare class gdAbstractFileSystemJS { +declare class gdAbstractFileSystemJS extends gdAbstractFileSystem { constructor(): void; mkDir(dir: string): void; dirExists(dir: string): void; diff --git a/GDevelop.js/types/gdobjectassetserializer.js b/GDevelop.js/types/gdobjectassetserializer.js new file mode 100644 index 000000000000..ce57a1f86094 --- /dev/null +++ b/GDevelop.js/types/gdobjectassetserializer.js @@ -0,0 +1,6 @@ +// Automatically generated by GDevelop.js/scripts/generate-types.js +declare class gdObjectAssetSerializer { + static serializeTo(project: gdProject, obj: gdObject, element: gdSerializerElement): void; + delete(): void; + ptr: number; +}; \ No newline at end of file diff --git a/GDevelop.js/types/gdprojectresourcescopier.js b/GDevelop.js/types/gdprojectresourcescopier.js index a208450a6be9..e60eb3cc7534 100644 --- a/GDevelop.js/types/gdprojectresourcescopier.js +++ b/GDevelop.js/types/gdprojectresourcescopier.js @@ -1,6 +1,7 @@ // Automatically generated by GDevelop.js/scripts/generate-types.js declare class gdProjectResourcesCopier { static copyAllResourcesTo(project: gdProject, fs: gdAbstractFileSystem, destinationDirectory: string, updateOriginalProject: boolean, preserveAbsoluteFilenames: boolean, preserveDirectoryStructure: boolean): boolean; + static copyObjectResourcesTo(project: gdProject, originalObject: gdObject, fs: gdAbstractFileSystem, destinationDirectory: string, objectFullName: string): boolean; delete(): void; ptr: number; }; \ No newline at end of file diff --git a/GDevelop.js/types/libgdevelop.js b/GDevelop.js/types/libgdevelop.js index 79ab87ddc39c..3a1d873217ac 100644 --- a/GDevelop.js/types/libgdevelop.js +++ b/GDevelop.js/types/libgdevelop.js @@ -122,6 +122,7 @@ declare class libGDevelop { SerializerElement: Class; SharedPtrSerializerElement: Class; Serializer: Class; + ObjectAssetSerializer: Class; InstructionsList: Class; Instruction: Class; Expression: Class; diff --git a/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/LocalEventsFunctionsExtensionWriter.js b/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/LocalEventsFunctionsExtensionWriter.js index c2aab31e7453..ac8e228bc53b 100644 --- a/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/LocalEventsFunctionsExtensionWriter.js +++ b/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/LocalEventsFunctionsExtensionWriter.js @@ -1,11 +1,19 @@ // @flow -import { serializeToJSObject } from '../../Utils/Serializer'; +import assignIn from 'lodash/assignIn'; +import { + serializeToJSObject, + serializeToObjectAsset, +} from '../../Utils/Serializer'; import optionalRequire from '../../Utils/OptionalRequire'; +import LocalFileSystem from '../../ExportAndShare/LocalExporters/LocalFileSystem'; +import { archiveLocalFolder } from '../../Utils/LocalArchiver'; const fs = optionalRequire('fs-extra'); const path = optionalRequire('path'); const remote = optionalRequire('@electron/remote'); const dialog = remote ? remote.dialog : null; +const gd: libGDevelop = global.gd; + const writeJSONFile = (object: Object, filepath: string): Promise => { if (!fs) return Promise.reject(new Error('Filesystem is not supported.')); @@ -28,6 +36,12 @@ const writeJSONFile = (object: Object, filepath: string): Promise => { } }; +const addSpacesToPascalCase = (pascalCaseName: string): string => { + let name = pascalCaseName.replace(/([A-Z]+[a-z]|\d+)/g, ' $1'); + name = name.charAt(0) === ' ' ? name.substring(1) : name; + return name; +}; + export default class LocalEventsFunctionsExtensionWriter { static chooseEventsFunctionExtensionFile = ( extensionName?: string @@ -63,7 +77,7 @@ export default class LocalEventsFunctionsExtensionWriter { }); }; - static chooseCustomObjectFile = (objectName?: string): Promise => { + static chooseObjectAssetFile = (objectName?: string): Promise => { if (!dialog) return Promise.reject('Not supported'); const browserWindow = remote.getCurrentWindow(); @@ -72,11 +86,12 @@ export default class LocalEventsFunctionsExtensionWriter { title: 'Export an object of the project', filters: [ { - name: 'GDevelop 5 object configuration', + name: 'GDevelop 5 object pack', extensions: ['gdo'], }, ], - defaultPath: objectName || 'Object', + defaultPath: + (objectName && addSpacesToPascalCase(objectName)) || 'Object', }) .then(({ filePath }) => { if (!filePath) return null; @@ -84,21 +99,59 @@ export default class LocalEventsFunctionsExtensionWriter { }); }; - static writeCustomObject = ( - customObject: gdObject, + static writeObjectsAssets = ( + project: gdProject, + exportedObjects: gdObject[], filepath: string ): Promise => { - const exportedObject = customObject.clone().get(); - exportedObject.getVariables().clear(); - exportedObject.getEffects().clear(); - exportedObject - .getAllBehaviorNames() - .toJSArray() - .forEach(name => exportedObject.removeBehavior(name)); - const serializedObject = serializeToJSObject(exportedObject); - return writeJSONFile(serializedObject, filepath).catch(err => { - console.error('Unable to write the object:', err); - throw err; + const localFileSystem = new LocalFileSystem({ + downloadUrlsToLocalFiles: true, + }); + const fileSystem = assignIn(new gd.AbstractFileSystemJS(), localFileSystem); + const temporaryOutputDir = path.join( + fileSystem.getTempDir(), + 'AssetExport' + ); + fileSystem.mkDir(temporaryOutputDir); + fileSystem.clearDir(temporaryOutputDir); + + return Promise.all( + exportedObjects.map(exportedObject => { + const clonedObject = exportedObject.clone().get(); + + gd.ProjectResourcesCopier.copyObjectResourcesTo( + project, + clonedObject, + fileSystem, + temporaryOutputDir, + addSpacesToPascalCase(clonedObject.getName()) + ); + + const serializedObject = serializeToObjectAsset(project, clonedObject); + // Resource names are changed by copyObjectResourcesTo so they don't + // match any project resource. + serializedObject.objectAssets.forEach(asset => + asset.resources.forEach(resource => { + resource.file = resource.name; + }) + ); + + return writeJSONFile( + serializedObject, + path.join( + temporaryOutputDir, + addSpacesToPascalCase(exportedObject.getName()) + '.asset.json' + ) + ).catch(err => { + console.error('Unable to write the object:', err); + throw err; + }); + }) + ).then(() => { + archiveLocalFolder({ + path: temporaryOutputDir, + outputFilename: filepath, + }); }); }; } diff --git a/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/index.js b/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/index.js index e4338381cda6..2952c7e1ed01 100644 --- a/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/index.js @@ -13,6 +13,10 @@ export type EventsFunctionsExtensionWriter = { extension: gdEventsFunctionsExtension, filepath: string ) => Promise, - chooseCustomObjectFile: (objectName?: string) => Promise, - writeCustomObject: (extension: gdObject, filepath: string) => Promise, + chooseObjectAssetFile: (objectName?: string) => Promise, + writeObjectsAssets: ( + project: gdProject, + exportedObjects: gdObject[], + filepath: string + ) => Promise, }; diff --git a/newIDE/app/src/ObjectEditor/ObjectExporterDialog.js b/newIDE/app/src/ObjectEditor/ObjectExporterDialog.js index 6518fbb937b5..fbd5c8b13ef1 100644 --- a/newIDE/app/src/ObjectEditor/ObjectExporterDialog.js +++ b/newIDE/app/src/ObjectEditor/ObjectExporterDialog.js @@ -12,11 +12,40 @@ import EventsFunctionsExtensionsContext, { type EventsFunctionsExtensionsState, } from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext'; import Window from '../Utils/Window'; +import { mapFor } from '../Utils/MapFor'; import Upload from '../UI/CustomSvgIcons/Upload'; -const exportCustomObject = async ( +const exportObjectAsset = async ( eventsFunctionsExtensionsState: EventsFunctionsExtensionsState, + project: gdProject, customObject: gdObject +) => { + await exportObjectsAssets( + eventsFunctionsExtensionsState, + project, + [customObject], + customObject.getName() + ); +}; + +const exportLayoutObjectAssets = async ( + eventsFunctionsExtensionsState: EventsFunctionsExtensionsState, + project: gdProject, + layout: gdLayout +) => { + await exportObjectsAssets( + eventsFunctionsExtensionsState, + project, + mapFor(0, layout.getObjectsCount(), i => layout.getObjectAt(i)), + layout.getName() + ); +}; + +const exportObjectsAssets = async ( + eventsFunctionsExtensionsState: EventsFunctionsExtensionsState, + project: gdProject, + objects: gdObject[], + defaultName: string ) => { const eventsFunctionsExtensionWriter = eventsFunctionsExtensionsState.getEventsFunctionsExtensionWriter(); if (!eventsFunctionsExtensionWriter) { @@ -25,14 +54,15 @@ const exportCustomObject = async ( "The object can't be exported because it's not supported by the web-app." ); } - const pathOrUrl = await eventsFunctionsExtensionWriter.chooseCustomObjectFile( - customObject.getName() + const pathOrUrl = await eventsFunctionsExtensionWriter.chooseObjectAssetFile( + defaultName ); if (!pathOrUrl) return; - await eventsFunctionsExtensionWriter.writeCustomObject( - customObject, + await eventsFunctionsExtensionWriter.writeObjectsAssets( + project, + objects, pathOrUrl ); }; @@ -44,6 +74,8 @@ const openGitHubIssue = () => { }; type Props = {| + project: gdProject, + layout: gdLayout, object: gdObject, onClose: () => void, |}; @@ -92,14 +124,28 @@ const ObjectExporterDialog = (props: Props) => { icon={} primary label={Export to a file} - onClick={() => { - exportCustomObject(eventsFunctionsExtensionsState, props.object); - }} + onClick={() => + exportObjectAsset( + eventsFunctionsExtensionsState, + props.project, + props.object + ) + } /> Submit objects to the community} onClick={openGitHubIssue} /> + Export all scene objects} + onClick={() => + exportLayoutObjectAssets( + eventsFunctionsExtensionsState, + props.project, + props.layout + ) + } + /> diff --git a/newIDE/app/src/ObjectsList/index.js b/newIDE/app/src/ObjectsList/index.js index 38a557925f11..6649ac988ef4 100644 --- a/newIDE/app/src/ObjectsList/index.js +++ b/newIDE/app/src/ObjectsList/index.js @@ -1415,8 +1415,7 @@ const ObjectsList = React.forwardRef( 'EffectCapability::EffectBehavior' ), }, - eventsFunctionsExtensionWriter && - project.hasEventsBasedObject(object.getType()) + eventsFunctionsExtensionWriter ? { label: i18n._(t`Export object`), click: () => onExportObject && onExportObject(object), diff --git a/newIDE/app/src/SceneEditor/index.js b/newIDE/app/src/SceneEditor/index.js index 7f3867510d4c..8865190610d7 100644 --- a/newIDE/app/src/SceneEditor/index.js +++ b/newIDE/app/src/SceneEditor/index.js @@ -1839,6 +1839,8 @@ export default class SceneEditor extends React.Component { {this.state.exportedObject && ( { this.openObjectExporterDialog(null); diff --git a/newIDE/app/src/Utils/Serializer.js b/newIDE/app/src/Utils/Serializer.js index b1ef2653bb92..cded4d789a48 100644 --- a/newIDE/app/src/Utils/Serializer.js +++ b/newIDE/app/src/Utils/Serializer.js @@ -24,6 +24,17 @@ export function serializeToJSObject( return object; } +export function serializeToObjectAsset(project: gdProject, object: gdObject) { + const serializedElement = new gd.SerializerElement(); + gd.ObjectAssetSerializer.serializeTo(project, object, serializedElement); + + // JSON.parse + toJSON is 30% faster than gd.Serializer.toJSObject. + const objectAsset = JSON.parse(gd.Serializer.toJSON(serializedElement)); + serializedElement.delete(); + + return objectAsset; +} + /** * Tool function to save a serializable object to a JSON. * Most gd.* objects are "serializable", meaning they have a serializeTo diff --git a/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectExporterDialog.stories.js b/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectExporterDialog.stories.js index 37c90d66bc3e..ac74d586dae8 100644 --- a/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectExporterDialog.stories.js +++ b/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectExporterDialog.stories.js @@ -36,6 +36,8 @@ export const Default = () => ( value={fakeEventsFunctionsExtensionsContext} > action('Close the dialog')} />